A call to /v1/decide sends everything every time: the instructions, the context and the questions. A session sends that fixed part once. The model keeps your context loaded for the life of the session, so each frame you stream costs only its own tokens, and each reply carries the same typed, calibrated answers as a normal call. Control loops covers when to use one.
POST /v1/sessionscoming sooninstructions, state, questions, window 8
sent once, billed once
Open a session#
https://api.decisionnode.com /v1/sessions{
"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
}# the body above, saved as session.json
curl https://api.decisionnode.com/v1/sessions \
-H "Authorization: Bearer $DECISIONNODE_API_KEY" \
-H "Content-Type: application/json" \
-d @session.jsonmodelstringrequired- Either model, or a pinned version, as on
/v1/decide. instructionsstring- Who the model is deciding as, for every frame of the session.
statestring | object | array- The static context: a mission brief, a rulebook, a map legend. Read once, within the 64k request limit.
questionsobjectrequired- The questions every frame is answered on, in the same shape as on
/v1/decide: choice, score, truth or number. windownumber- How many recent frames stay in context. Default 8.
ttl_secondsnumber- How long the session lives. Default 300, at most 3,600.
What comes back
session_idstring- The session's id. Use it to open the stream and to end the session.
The response also carries the stream URL (wss://api.decisionnode.com/) and the limits that apply to each frame. Their exact field names are published at release.
Stream frames#
wss://api.decisionnode.com /v1/sessions/{id}/streamConnect from your server with your key in the Authorization header (Bearer dn_live_...), as on every call. Each message you send is one frame; each message you receive is the reply to one frame.
{
"seq": 17,
"frame": {
"battery": 0.31,
"wind_mps": 11.4,
"distance_home_m": 820,
"obstacle": "none"
}
}Frame
seqnumberrequired- Your sequence number for the frame. The reply carries it back.
framestring | object | imagerequired- What changed: text, a JSON value or one image, at most 4,096 tokens. How an image frame is encoded is published at release.
questionsstring[]- The question names to answer this time. All of them when left out.
resetbooleantruedrops the recent frames from context before this one is answered.
Replies#
{
"seq": 17,
"answers": {
"mode": {
"type": "choice",
"choice": "return",
"confidence": 0.79,
"probabilities": {
"continue": 0.09,
"hold": 0.05,
"land": 0.02,
"return": 0.84
}
},
"abort": { "type": "truth", "truth": 0.07 }
}
}Reply
seqnumber- The sequence number of the frame this answers.
answersobject- One answer per question asked, exactly as
/v1/decideshapes it, with the same calibration. latency_msnumber- Time spent on our side for this frame, in milliseconds.
safetyobject- The safety check's probabilities for this frame. Every frame passes the same check as every call.
How a session behaves#
- Rolling window. Each frame is answered on the fixed part plus the last
windowframes (8 by default), so the model sees what just happened. Older frames leave the context. Send"reset": trueto start the window over. - Latest wins. If frames arrive faster than they are answered, the newest frame is answered and the skipped sequence numbers are reported to you. A client that falls behind gets the newest answer, never a backlog. If you need every frame answered, send the next one after its reply.
- Lifetime. A session ends at
ttl_seconds(300 by default, at most 3,600), when the socket closes, or when you end it. If it ends early, the socket closes with a code your client can read: open a new session with the same body and carry on. - One session per socket. Run several sessions on several sockets, within the sessions your key may hold.
End a session#
https://api.decisionnode.com /v1/sessions/{id}curl -X DELETE https://api.decisionnode.com/v1/sessions/$SESSION_ID \
-H "Authorization: Bearer $DECISIONNODE_API_KEY"Ending a session frees its context at once. Closing the socket ends it too.
Limits#
| Limit | Value |
|---|---|
| Frame | At most 4,096 tokens, or one image |
| Fixed part | Within the 64k request limit, as on Limits |
| Window | 8 recent frames by default |
| Lifetime | 300 seconds by default, at most 3,600 |
| Sessions per key | Published at /v1/models |
Billing#
session = opening tokens × price
+ sum(frame tokens × price)
- opening tokens
- the instructions, the state and the questions, billed once when the session opens
- frame tokens
- each frame's own tokens plus the questions it asks
- price
- the live price per input token of the session's model, today $0.042 per million on DecisionNode-1.0
Output stays free. The context is never billed again per frame, which is what makes a long session cheap. See Pricing and billing.
Clients#
Open the session over HTTPS, then stream. One task sends frames at your sensor's rate; another reads replies and hands each typed answer to your own controller, which keeps flying, driving or running while it waits.
import asyncio, json, os
import websockets # pip install websockets
KEY = os.environ["DECISIONNODE_API_KEY"]
MODE_AT, ABORT_AT = 0.7, 0.9 # your thresholds, in config
async def supervise(session_id, telemetry, autopilot):
url = f"wss://api.decisionnode.com/v1/sessions/{session_id}/stream"
headers = {"Authorization": f"Bearer {KEY}"}
async with websockets.connect(url, additional_headers=headers) as ws:
async def send_frames():
seq = 0
async for reading in telemetry: # your sensor loop, at its own rate
seq += 1
await ws.send(json.dumps({"seq": seq, "frame": reading}))
async def read_replies():
async for message in ws:
reply = json.loads(message)
# under latest wins a reply may also list the seqs it skipped
# (the field is published at release); the newest answer is what counts
mode = reply["answers"]["mode"]
abort = reply["answers"]["abort"]
# the autopilot flies; the session only proposes a mode
if abort["truth"] >= ABORT_AT:
autopilot.request_mode("return", seq=reply["seq"])
elif mode["confidence"] >= MODE_AT:
autopilot.request_mode(mode["choice"], seq=reply["seq"])
# below both bars the autopilot keeps its current mode
await asyncio.gather(send_frames(), read_replies())Published at release#
- Measured per-frame latency for both models.
- The full open response, beyond
session_idand the stream URL. - How an image frame is encoded, and how skipped frames are listed in a reply.
- Close codes and error bodies.
- The sessions each key may hold, at
/v1/models.