https://api.decisionnode.com /v1/batchesAPI=https://api.decisionnode.com/v1
curl "$API/batches" \
-H "Authorization: Bearer $DECISIONNODE_API_KEY" \
-H "Content-Type: application/x-ndjson" \
--data-binary @tickets.jsonlAuthentication#
AuthorizationheaderrequiredBearer dn_live_..., the same key as every call. A batch belongs to the key that created it: other keys, even in the same workspace, get404for it.Content-Typeheaderrequiredapplication/x-ndjsonfor JSON Lines,application/jsonfor an array. The API also reads the first byte:[opens an array,{a line.
Request body#
JSON Lines, one request per line, or a JSON array of the same objects. Each request is a /v1/decide body plus custom_id. An empty body or [] creates an empty draft you fill with Add requests.
{"custom_id":"ticket-48213","model":"decisionnode-latest","state":"Customer: I was charged twice and nobody has replied for 3 days.","questions":{"route":{"type":"choice","instructions":"Where should this go?","criteria":{"billing":"money","bug":"broken","account":"login"}},"refund":{"type":"truth","instructions":"Refund this automatically?"}}}
{"custom_id":"ticket-48214","model":"decisionnode-latest","state":"Customer: The app crashes every time I open the invoices tab.","questions":{"route":{"type":"choice","instructions":"Where should this go?","criteria":{"billing":"money","bug":"broken","account":"login"}},"refund":{"type":"truth","instructions":"Refund this automatically?"}}}
{"custom_id":"ticket-48215","model":"decisionnode-latest","state":"Customer: My login stopped working after I changed my email address.","questions":{"route":{"type":"choice","instructions":"Where should this go?","criteria":{"billing":"money","bug":"broken","account":"login"}},"refund":{"type":"truth","instructions":"Refund this automatically?"}}}Each request
custom_idstringrequired- Your id for the request: 1 to 512 characters, unique in the batch. Its result line carries it back.
modelstringdefault"decisionnode-latest"decisionnode-latest,decisionnode-flash-latestor a pinned version, and left out it isdecisionnode-latest, exactly as on/v1/decide. It sets the line's price. One batch may mix models. An unknown name makes that line a failed line with the live400; the rest of the batch runs.statestring | object | array- The input to decide about, as on
/v1/decide. imagesarray- Images, base64 in
data, as on/v1/decide. They count toward the upload size. questionsobjectrequired- The questions, as on
/v1/decide: choice, score, truth or number.
Response#
201 Created with the batch object in state draft. counters.requests counts every line; lines refused for their shape are already counted in failed.
{
"id": "batch_01J9ZB8Q3T",
"state": "draft",
"counters": {
"requests": 3,
"queued": 0,
"in_progress": 0,
"completed": 0,
"failed": 0,
"expired": 0,
"cancelled": 0
},
"created_at": "2026-10-06T09:12:04Z",
"finalized_at": null,
"expires_at": null,
"ended_at": null,
"results_url": null,
"results_expire_at": null,
"price_factor": 0.5,
"usage": { "input_tokens": 0, "output_tokens": 0 },
"error": null
}API=https://api.decisionnode.com/v1curl "$API/batches" \ -H "Authorization: Bearer $DECISIONNODE_API_KEY" \ -H "Content-Type: application/x-ndjson" \ --data-binary @tickets.jsonlErrors#
| Status | Type | When | What to do |
|---|---|---|---|
400 | api_usage_error | The body is neither JSON Lines nor a JSON array; a line is not a JSON object; a custom_id is missing, empty, too long or repeated; more than 10,000 requests | Fix the upload and send it again. Nothing was created |
401 | authentication_error | The key is missing, unknown or revoked | Check the Authorization header |
413 | request_too_large_error | The upload is over 256 MB | Split it into several batches of up to 10,000 requests |
429 | batch_limit_exceeded | Your key already has 10 active batches | Finalize or cancel a draft, or wait for a batch to end |
429 | batch_limit_exceeded | The call would put more than 100,000 requests in your key's active batches, drafts included | Wait for a batch to end, or cancel a draft you will not finalize. Nothing from the call was applied |
429 | rate_limit_error | Over your request rate: batch calls count against it | Wait Retry-After seconds, then send the same call again |
500 | api_error | A fault on our side | Retry with backoff and jitter. A 5xx can come after the call took effect: before you create again, cancel any draft the failed create left behind (see Batch errors) |
529 | overloaded_error | The API cannot take the call right now | Wait Retry-After seconds, then retry with backoff |
Every error body is {"detail": {"error_type": "...", "message": "..."}}. Retry 408, 429 (rate_limit_error), 529 and any 5xx, waiting at least what Retry-After says when it is set; fix the call for the other 4xx. A 429 batch_limit_exceeded clears only when a batch ends or you cancel one. All the bodies are on Errors.
Limits#
| Limit | Value |
|---|---|
| Requests | Up to 10,000 |
| Upload | 256 MB, images included |
custom_id | 1 to 512 characters, unique in the batch |
| Each request | The live limits of /v1/decide |
| Requests in active batches per key | 100,000, drafts included, checked on this call |
Notes#
- A line the API refuses for its shape (an unknown model, a question with no options, a field of the wrong type) is accepted into the batch as a failed line with the live status and body, and never runs. The rest of the batch is unaffected.
- Limits counted in tokens, such as the per-request token limit, are checked when the line runs: such a line comes back as the live
400. - A refused upload leaves nothing behind: no draft, no lines.
- The draft counts toward your 10 active batches until you finalize or cancel it.