POST
https://api.decisionnode.com /v1/sessions# session.json holds the body below
curl https://api.decisionnode.com/v1/sessions \
-H "Authorization: Bearer $DECISIONNODE_API_KEY" \
-H "Content-Type: application/json" \
-d @session.jsonReads the instructions, the state and the questions once, keeps them loaded for the life of the session, and bills them once. Every frame you stream afterwards is answered on them.
Authentication#
AuthorizationheaderrequiredBearer dn_live_..., the same key as every call. A session belongs to the key that opened it: other keys, even in the same workspace, get404for it (close code4404on the stream).Content-Typeheaderrequiredapplication/json.
Request body#
{
"model": "decisionnode-latest",
"instructions": "You are the mission supervisor of a survey drone.",
"state": {
"mission": "Survey the north field in parallel lines, 40 m altitude",
"geofence": "Stay inside the field boundary and below 120 m",
"rules": "Return home when battery is under 30% at the far end of a line"
},
"questions": {
"mode": {
"type": "choice",
"instructions": "Which flight mode now?",
"criteria": {
"continue": "keep flying the survey pattern",
"hold": "hover in place until conditions change",
"return": "fly back to the home point",
"land": "land at the nearest safe spot now"
}
},
"abort": {
"type": "truth",
"instructions": "Should the mission be aborted now?"
}
},
"window": 8
}modelstringdefault"decisionnode-latest"decisionnode-latestordecisionnode-flash-latest, or a pinned version such asdecisionnode-1.0, as on/v1/decide. Every frame of the session is answered by this model. Any other name is a400.instructionsstring | object | array- Who the model decides as, for every frame of the session. Read once, with the state.
statestring | object | arraydefault""- The static context: a mission brief, a rulebook, a map legend. Read once when the session opens and kept loaded for its life.
questionsobjectrequired- The questions every frame is answered on, in the same shape as on
/v1/decide: choice, score, truth or number. Their keys are the names a frame'squestionslist uses. windowintegerdefault8- How many frames each frame is answered with, the frame itself included, beside the fixed part: 1 to 64.
1answers each frame on the fixed part and that frame alone. When the recent frames together pass the session'swindow_tokens, the oldest leave first. Outside 1 to 64 is a422. ttl_secondsintegerdefault300- How long the session lives from the moment it opens, 1 to 3,600; outside that is a
422. Activity does not extend it.
Response#
200 OK with the session. Connect to stream_url next; see Stream frames.
{
"session_id": "ses_3f9a1c07b2e84d5f6a0c9e12",
"model": "decisionnode-1.0",
"stream_url": "wss://api.decisionnode.com/v1/sessions/ses_3f9a1c07b2e84d5f6a0c9e12/stream",
"window": 8,
"ttl_seconds": 300,
"expires_at": "2026-10-06T09:35:00Z",
"limits": {
"max_frame_tokens": 1535,
"max_frame_images": 1,
"window_tokens": 1536,
"idle_timeout_seconds": 120
},
"questions": ["mode", "abort"],
"prefix_tokens": 52,
"usage": { "input_tokens": 164, "output_tokens": 0 },
"answers": {
"mode": {
"type": "choice",
"choice": "continue",
"confidence": 0.84,
"probabilities": {
"continue": 0.88,
"hold": 0.06,
"land": 0.01,
"return": 0.05
}
},
"abort": { "type": "truth", "truth": 0.03 }
},
"safety": {
"checked": true,
"harm": 0.0018,
"self_harm": 0.0007,
"flagged": []
}
}session_idstring- The session's id:
ses_and 24 hex digits, such asses_3f9a1c07b2e84d5f6a0c9e12. Treat it as opaque. Use it to end the session. modelstring- The pinned version that answers every frame, for example
decisionnode-1.0. stream_urlstring- The WebSocket to connect to:
wss://api.decisionnode.com/with this session's id.v1/ sessions/ {id}/ stream windowinteger- The window in force.
ttl_secondsinteger- The lifetime in force.
expires_atstring- When it ends unless you end it first:
ttl_secondsafter this call, ISO 8601 in UTC. limitsobject- This session's own limits.
max_frame_tokensinteger- Tokens of text or JSON per frame in this session: at most 4,096, and less when
window_tokensis smaller, since a frame never holds more than the window's room. max_frame_imagesinteger- Images per frame, 1.
window_tokensinteger- Tokens the recent frames may hold together. Past it, the oldest frames leave the window first.
idle_timeout_secondsinteger- How long the socket may go without a frame, 120. Past it the socket closes with
4408; pings do not count as frames.
questionsstring[]- The question keys, in order: the names a frame's
questionslist uses. prefix_tokensinteger- The tokens of the instructions and the state.
usageobject- The fixed part with every question, billed once now:
input_tokens, andoutput_tokens, always 0. answersobject- Every question answered on the fixed part alone, before any frame, in the shapes of
/v1/decide. safetyobject- What the safety check found for the fixed part, in the same shape as on every reply.
# session.json holds the body belowcurl https://api.decisionnode.com/v1/sessions \ -H "Authorization: Bearer $DECISIONNODE_API_KEY" \ -H "Content-Type: application/json" \ -d @session.jsonErrors#
| Status | Type | When | What to do |
|---|---|---|---|
400 | api_usage_error | An unknown model or question type, a field the body does not take, a question over its limits, images in the body (images go in frames), or a model whose sessions.enabled is false | Fix the body; the message names the problem |
400 | max_tokens_exceeded | The fixed part, with room for the window and the longest question, does not fit: keep it within sessions.max_prefix_tokens | Trim the state or move detail into frames; x-decisionnode-error has the count |
401 | authentication_error | The key is missing, unknown or revoked | Check the Authorization header |
402 | insufficient_credit | Your prepaid balance is empty | Add credit, then open the session |
403 | refusal_error | The safety check refused the fixed part; the body names the category | Do not retry; change what the session asks. See Safety check |
408 | request_timeout_error | The body stopped arriving for 20 seconds, or took over 300 | Retry on a working connection, with backoff |
413 | request_too_large_error | The body is over 64 MB | Trim the state; send detail that changes in frames |
422 | a detail list | A field is missing or has the wrong type, window outside 1 to 64, ttl_seconds outside 1 to 3,600, a number grid that cannot be built, or the body is not JSON | Fix the fields listed in detail |
429 | rate_limit_error | Your key already holds sessions.max_per_key open sessions | End a session you no longer use, then open again |
429 | rate_limit_error | Over your key's request rate | Wait Retry-After seconds, then retry |
500 | api_error | A fault on our side | Retry with backoff and jitter; it is always safe |
529 | overloaded_error | No capacity right now, or no room for another session | Wait Retry-After seconds, then retry with backoff |
Every error body is {"detail": {"error_type": "...", "message": "..."}}, or a list of fields for a 422. Retry 408, 429, 529 and any 5xx, waiting at least what Retry-After says when it is set; fix the request for the other 4xx. All the bodies are on Errors.
Limits#
| Limit | Value |
|---|---|
| Fixed part | At most sessions.max_prefix_tokens at /v1/models, with room left for the window and the longest question |
window | 1 to 64, 8 by default |
ttl_seconds | 1 to 3,600, 300 by default |
| Open sessions per key | sessions.max_per_key at /v1/models |
Notes#
- The session counts toward your open sessions from this call until it ends, whether or not a socket is connected. End sessions you do not use.
- Connect soon after this call:
expires_atcounts from here, not from the connect. Once a socket is connected, it closes afteridle_timeout_seconds(120 seconds) without a frame. - Opening the same body twice opens two sessions, each billed for its fixed part. If an open call times out and you open again, a session the first call may have opened gets no socket and ends at its
expires_at; until then it counts toward your open sessions. - The safety check reads the fixed part once, here; every frame is checked again when it arrives.