Skip to content
DecisionNodeDecisionNdedocs
  • Guides
  • API reference
  • Examples
  • Playground

start here

  • QuickstartGet startedGet an API key, send one request with three questions, and branch your code on the typed answers. Plain HTTPS, no SDK to install.
  • POST /v1/decideAPI referenceAnswer typed questions about a state and optional images. One request, one buffered JSON response, one answer per question.
  • QuestionsConceptsQuestions say what to decide. Each one has a type that fixes the shape of its answer: a choice from your options, a score on your scale, a…
  • ConfidenceConceptsProbabilities are calibrated per question type, so a threshold means what it says.
  • ImagesConceptsSend images and text in the same request. The model reads printed and handwritten text, amounts, dates, objects and layout, and answers…
  • Batch jobsPatternsSend up to 10,000 requests in one file and collect the answers later, at half the live price.
  • Pricing and billingYou pay for input tokens only. Output is free because the model generates no text.
↑↓ moveopen7 suggestions
Get API keyGet API key
DecisionNodeDecisionNde

Get started

  • Introduction
  • Quickstart
  • Playground
  • Console and keys
  • With coding agents
  • Examples

Concepts

  • State
  • Questions
  • Choice
  • Score
  • Truth
  • Number
  • Images
  • Confidence
  • Determinism

Models

  • DecisionNode-1.0
  • DecisionNode-1.0 Flash
  • Limits
  • Versions
  • Dedicated capacity

Patterns

  • Confidence-gated routing
  • Fan-out
  • Guardrails
  • Control loops
  • Batch jobs

API reference

  • Overview
  • POST/v1/decide
  • GET/v1/models
  • Errors
  • Safety check
  • Rate limits

Sessions API

  • Sessions overview
  • POSTOpen a session
  • WSStream frames
  • DELEnd a session

Batch API

  • The batch object
  • POSTCreate a batch
  • POSTAdd requests
  • POSTFinalize a batch
  • GETRetrieve a batch
  • GETGet batch results
  • POSTCancel a batch
  • GETList batches

Pricing and billing

  • Pricing and billing

Policies

  • Responsible use
  • Data and privacy

Migrate

  • Coming from a Jev-shaped API
  • Benchmarks
  • Pricing
  • Playground
Get API key
  • Guides
  • API reference
  • Examples
  • Playground

Get started

  • Introduction
  • Quickstart
  • Playground
  • Console and keys
  • With coding agents
  • Examples

Concepts

  • State
  • Questions
  • Choice
  • Score
  • Truth
  • Number
  • Images
  • Confidence
  • Determinism

Models

  • DecisionNode-1.0
  • DecisionNode-1.0 Flash
  • Limits
  • Versions
  • Dedicated capacity

Patterns

  • Confidence-gated routing
  • Fan-out
  • Guardrails
  • Control loops
  • Batch jobs

API reference

  • Overview
  • POST/v1/decide
  • GET/v1/models
  • Errors
  • Safety check
  • Rate limits

Sessions API

  • Sessions overview
  • POSTOpen a session
  • WSStream frames
  • DELEnd a session

Batch API

  • The batch object
  • POSTCreate a batch
  • POSTAdd requests
  • POSTFinalize a batch
  • GETRetrieve a batch
  • GETGet batch results
  • POSTCancel a batch
  • GETList batches

Pricing and billing

  • Pricing and billing

Policies

  • Responsible use
  • Data and privacy

Migrate

  • Coming from a Jev-shaped API
  1. docs
  2. /
  3. API reference

Safety check

Every request to the hosted API is checked and, unless it is refused, answered. Each response says how likely the request is to seek harm to people or help to self-harm. By default only a request that very likely seeks help to self-harm is refused; your key can be set to flag or refuse more, never less.

on this page9 sections
  1. What it checks
  2. Answer always, flag always
  3. The safety header
  4. The safety body
  5. Your key's policy
  6. When a request is refused
  7. When the check cannot run
  8. Batches and sessions
  9. What is kept
Runs on
Every /v1/decide call, every batch line, and the opening of a session and each of its frames
Categories
harm (choosing or targeting people to be hurt) and self_harm (help to hurt oneself)
Default for every key
Flag both categories; refuse a request whose self_harm probability is above 0.9
Where you see it
The x-decisionnode-safety header on every decision response, and on request a safety object in the body
A refusal
403 with error_type refusal_error and the category; not billed
Cost
The check itself is never billed and never counts toward usage or a limit

What it checks#

The check answers two questions about every request, as calibrated probabilities from 0 to 1. It reads what your request asks the model to decide: the questions, their options and their criteria, together with the state.

  • harm

    Covers
    Choosing, ranking or targeting people to be physically hurt or killed, or choosing whom to harm by a protected attribute such as race, religion, sex or disability
    Not in it
    Harmful content in general, risk detection, content moderation, fraud checks, hiring, credit and the triage of one patient
  • self_harm

    Covers
    Helping a person to hurt or kill themselves, such as choosing a method, a place or a dose
    Not in it
    Asking whether a message shows a risk of self-harm, and routing a person to help
The two categories
CategoryCoversNot in it
harmChoosing, ranking or targeting people to be physically hurt or killed, or choosing whom to harm by a protected attribute such as race, religion, sex or disabilityHarmful content in general, risk detection, content moderation, fraud checks, hiring, credit and the triage of one patient
self_harmHelping a person to hurt or kill themselves, such as choosing a method, a place or a doseAsking whether a message shows a risk of self-harm, and routing a person to help

Not a content classifier

The check asks whom a decision would harm, not whether a text is unpleasant. A request that asks whether a post is a threat, whether a message shows a risk of self-harm or how severe a comment is gets its answers as usual. To moderate content, ask your own question: a Score for severity or a Truth for a policy breach.

Answer always, flag always#

  • Every request is answered unless it is refused under your key's policy or the default block. The answers do not depend on your policy: two keys with different settings get the same answers for the same request.
  • Every response says what the check found, in the x-decisionnode-safety header, a 403 included.
  • The response body stays the same shape. Without the opt-in header below, a 200 body holds only model, answers and usage, so clients written for a Jev-shaped API read it unchanged.

The safety header#

x-decisionnode-safety is a structured header (RFC 8941 dictionary): each category with its probability to three decimals, a category above your key's threshold marked ;flagged, and the category that caused a refusal marked ;blocked. When the check did not run, the header says unchecked with a reason code.

  • harm=0.031, self_harm=0.004

    Meaning
    Checked; nothing above your thresholds
  • harm=0.912;flagged, self_harm=0.004

    Meaning
    Checked; harm is above your key's flag threshold. The request was answered
  • harm=0.004, self_harm=0.953;blocked

    Meaning
    Checked and refused for self_harm (on the 403)
  • unchecked;reason="no-model-question"

    Meaning
    Nothing for the model to decide: every question has a single possible answer, such as a choice with one option
  • unchecked;reason="does-not-fit"

    Meaning
    The check's own reading of the request is longer than the model's limit
  • unchecked;reason="no-answer"

    Meaning
    The check gave no usable answer
What the header can say
ValueMeaning
harm=0.031, self_harm=0.004Checked; nothing above your thresholds
harm=0.912;flagged, self_harm=0.004Checked; harm is above your key's flag threshold. The request was answered
harm=0.004, self_harm=0.953;blockedChecked and refused for self_harm (on the 403)
unchecked;reason="no-model-question"Nothing for the model to decide: every question has a single possible answer, such as a choice with one option
unchecked;reason="does-not-fit"The check's own reading of the request is longer than the model's limit
unchecked;reason="no-answer"The check gave no usable answer

Read the probabilities by name: more categories may be added, and the order is not fixed.

def parse_safety(header: str) -> dict | None:
    """'harm=0.912;flagged, self_harm=0.004' -> {"harm": (0.912, {"flagged"}), ...}"""
    if not header or header.startswith("unchecked"):
        return None  # the check did not run; the reason is in the header
    result = {}
    for item in header.split(","):
        name, _, rest = item.strip().partition("=")
        value, *marks = rest.split(";")
        result[name] = (float(value), set(marks))
    return result


def audit_if_flagged(response) -> None:
    """hold_for_audit is your own function."""
    safety = parse_safety(response.headers.get("x-decisionnode-safety", ""))
    if safety and "flagged" in safety["harm"][1]:
        hold_for_audit(response.headers["x-request-id"])

The safety body#

Send the request header x-decisionnode-safety-body: true (1 and yes work too) and a 200 body carries one more top-level field, safety, with the probabilities to four decimals. It is a request header, not a body field, so the same request works on every path and with clients that refuse unknown fields.

curl https://api.decisionnode.com/v1/decide \
  -H "Authorization: Bearer $DECISIONNODE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-decisionnode-safety-body: true" \
  -d '{
    "model": "decisionnode-latest",
    "state": "Customer: I was charged twice and nobody has replied for 3 days.",
    "questions": {
      "refund": {
        "type": "truth",
        "instructions": "Refund this automatically?"
      }
    }
  }'
200 OK
{
  "model": "decisionnode-1.0",
  "answers": {
    "refund": { "type": "truth", "truth": 0.94 }
  },
  "usage": { "input_tokens": 31, "output_tokens": 0 },
  "safety": {
    "checked": true,
    "harm": 0.0012,
    "self_harm": 0.0006,
    "flagged": []
  }
}

The safety object

checkedboolean
true when the check ran. When it is false, the object holds only reason.
harmnumber
The probability that the request asks to choose, rank or target people to be harmed, 0 to 1.
self_harmnumber
The probability that the request seeks help to harm oneself, 0 to 1.
flaggedstring[]
The categories above your key's thresholds. Empty when none are.
reasonstring
Only when checked is false: no-model-question, does-not-fit or no-answer, as in the header.

Session replies always carry this object, one per frame. Batch result lines carry the header's value in their headers.

Your key's policy#

Each key has a policy: for each category an action and a threshold. Flag marks the category in the header and the body when its probability is above the threshold, and still answers. Block refuses the request with a 403 above the threshold. The policy applies to live calls, batches and sessions alike.

  • The default

    Rule
    Flag harm and self_harm; refuse self_harm above 0.9. This applies to every key
  • harm

    Rule
    Flag or block, at a threshold you choose
  • self_harm

    Rule
    Flag or block, at a threshold you choose. A block can only be stricter than the default, at 0.9 or below; it can never be loosened or switched off
  • harm off

    Rule
    Only on the named keys of a signed Defence Contract Addendum, set by us. No other key can switch a category off
What a key's policy can be
SettingRule
The defaultFlag harm and self_harm; refuse self_harm above 0.9. This applies to every key
harmFlag or block, at a threshold you choose
self_harmFlag or block, at a threshold you choose. A block can only be stricter than the default, at 0.9 or below; it can never be loosened or switched off
harm offOnly on the named keys of a signed Defence Contract Addendum, set by us. No other key can switch a category off

A policy only tightens: where we have set a block on your key, your own setting applies on top of it and our block stays. To set or change your key's policy, write to hello@bynn.com from your account's contact address with the key's name and the settings you want; settings are made on written request and confirmed back to you in writing. No request field can change the policy.

When a request is refused#

A refused request gets 403 instead of answers. The body names the category, x-decisionnode-refusal repeats it, the safety header marks it ;blocked, and x-decisionnode-billable-tokens says what the refusal bills: 0. The same request is refused again, so never retry it.

{
  "detail": {
    "error_type": "refusal_error",
    "message": "This request was refused under the usage policy: it asks for help to harm oneself. If someone may be in danger, contact local emergency services or a crisis line.",
    "category": "self_harm"
  }
}

Branch on error_type and category, not on the message: the message wording may change. A self_harm refusal's message points to emergency services or a crisis line; if your product talks to people, show them your own route to help as well.

import requests


def handle_refusal(response: requests.Response, user) -> bool:
    """True when the safety check refused the request, after acting on it.
    log_refusal and show_crisis_resources are your own functions."""
    if response.status_code != 403:
        return False
    detail = response.json().get("detail", {})
    if detail.get("error_type") != "refusal_error":
        return False
    # refused under the usage policy; not billed, and refused again if resent
    log_refusal(detail["category"], response.headers["x-request-id"])
    if detail["category"] == "self_harm":
        show_crisis_resources(user)  # your product's route to human help
    return True

A 403 with error_type refusal_error is always this refusal: a missing, unknown or revoked key gets 401, and a paused workspace gets 403 with workspace_suspended and no category (see Errors). A 5xx or a 529 is never a refusal.

When the check cannot run#

  • A key that only flags gets its answers as usual, with unchecked and the reason in the header.
  • A key that blocks a category is never answered unchecked: the request gets 500 with error_type api_error. It is safe to retry, and the same request usually succeeds.
  • A request with nothing for the model to decide (every question has one possible answer) is answered with the header marked unchecked for the reason no-model-question, whatever the policy.

Batches and sessions#

  • Batch jobs. Every line is checked like a live call, under the same default block and your key's policy. A refused line has status 403 and the live body in its result, and is not billed. See Get batch results.
  • Sessions. The request that opens a session and every frame are checked. Every reply carries the safety object. A refused frame gets a frame error with refusal_error and never joins the window, and the socket stays open for the next frame. Unlike a refused call, a refused frame is billed: the model read it within the session. See Stream frames.

What is kept#

Records of flagged and refused requests hold the probabilities, the action taken, the key's id, the request id and the token counts. They never hold the text of a request: no state, question, option or image. See Data and privacy.

The rules behind the check

The check is a safety measure, not a permission: a request it answers is not, for that reason, an allowed use. Section 9 of the Acceptable Use Policy is the binding text, and Responsible use explains the rest of the rules.

previousErrorsnextRate limits

DecisionNode is built and run by Bynn Intelligence, Inc.

  • Home
  • Playground
  • Examples
  • Console
  • Responsible use
  • Terms
  • Acceptable use
  • Privacy
  • Data processing
  • Defence addendum
  • Cookies

on this page

  1. What it checks
  2. Answer always, flag always
  3. The safety header
  4. The safety body
  5. Your key's policy
  6. When a request is refused
  7. When the check cannot run
  8. Batches and sessions
  9. What is kept