Errors
Work out what went wrong and what to do about it: the error body, the status codes the API returns, and how to handle each in the SDK and over MCP.
Every failed request returns a JSON body with an error field and an HTTP status code:
{ "error": "run already in a terminal state" }The status code tells you the class of failure; the error string is a short, human-readable
reason.
Success codes
curl -sS -o /dev/null -w "%{http_code}\n" -X POST "$DURATON_URL/events" \
-H "Authorization: Bearer $DURATON_API_KEY" \
-d '{"name":"ticket.created","app":"support-app","data":{"ticketId":"T-421"}}'
# 202| Status | Returned by |
|---|---|
200 OK | Every read, the control writes (cancel, pause, resume, replay, retry-from-step, fork, bulk-replay), an approval decision, a PATCH on a webhook endpoint/source, and POST /webhook-source-deliveries/{id}/replay (returns the replay outcome). |
201 Created | POST /runs/{id}/scores, POST /datasets, POST /datasets/{id}/items, POST /datasets/{id}/eval, POST /webhook-endpoints, POST /webhook-sources. |
202 Accepted | POST /events and POST /webhooks/{token} - the event is recorded; the runs it starts are asynchronous. |
204 No Content | DELETE /webhook-endpoints/{id}, DELETE /webhook-sources/{id}, POST /webhook-deliveries/{id}/redeliver. |
Status codes
| Status | Meaning |
|---|---|
400 Bad Request | The request body is malformed, or a field is invalid - an event names a workflow that isn't registered, an event carries an unsupported character, an approval decision isn't approved/denied, a score is malformed, a dataset has no items to evaluate, or a GET /webhook-source-deliveries?status= value is outside the accepted set. |
401 Unauthorized | The API key is missing or unknown. |
403 Forbidden | A read-only (public) key attempted a write, or the project is suspended after its workspace reached the plan limit. |
404 Not Found | The addressed resource - run, step, event, approval, dataset, dataset item, eval run, or webhook source/endpoint/delivery - does not exist. |
409 Conflict | The request conflicts with current state (see below). |
501 Not Implemented | The requested capability isn't configured for this project (for example, failure explanations when no model is wired up). |
502 Bad Gateway | The status reserved for a runner-not-registered condition. Note that triggering a run against an app with no live runner does not return this: POST /events is still accepted (202) and the run parks and retries, failing terminally only after the bounded no-runner wait elapses. |
500 Internal Server Error | An unexpected fault. The body is a generic { "error": "internal error" }; the detail is logged server-side and never returned. |
Conflicts (409)
A 409 means the resource is in a state that doesn't allow the operation. The common cases:
error | Cause |
|---|---|
run already in a terminal state | Pausing, resuming, or cancelling a run that already succeeded, failed, or was cancelled. |
run is not in a terminal state | Replaying or forking a run that hasn't finished. |
run is not paused | Resuming a run that isn't paused. |
run is not awaiting a decision | Deciding an approval on a run that isn't waiting for one. |
run is not in a failed state | Explaining a run that didn't fail. |
approval is not open | Approving or denying an approval that was already decided. |
step attempt already recorded | A duplicate step result (a runner retried after Duraton already recorded the step). |
webhook token already in use | Creating an inbound source whose issued receive URL token collides (rare - the token is random). |
webhook source name already in use / webhook endpoint name already in use | A source/endpoint name isn't unique in the project. |
webhook endpoint url already subscribed | The outbound URL is already registered in the project. |
webhook delivery is in flight | Redelivering a webhook whose current attempt hasn't settled. |
dataset name already in use | Creating a dataset whose name is taken in the project. |
Conflicts are safe to surface to the caller and, where they reflect a race (a duplicate step, an in-flight delivery), safe to ignore.
Handling errors
Check the status code and read the error field:
curl -sS -o /tmp/body -w "%{http_code}" \
-X POST "$DURATON_URL/runs/$RUN_ID/resume" \
-H "Authorization: Bearer $DURATON_API_KEY"
# 409
cat /tmp/body
# { "error": "run is not paused" }The client throws DuratonApiError, which carries the status and raw body and has helpers for the
common classes:
import { createClient, DuratonApiError } from "@duraton/sdk/client";
const duraton = createClient({
url: process.env.DURATON_URL!,
apiKey: process.env.DURATON_API_KEY!,
});
try {
await duraton.runs.resume(runId);
} catch (err) {
if (err instanceof DuratonApiError) {
if (err.isConflict()) {
// 409: the run wasn't paused - already resumed or finished
} else if (err.isNotFound()) {
// 404
} else {
console.error(err.status, err.body);
}
// also: err.isBadRequest(), err.isUnauthorized(), err.isForbidden()
}
}When a tool call fails, the MCP server returns the same reason as the tool result's
error - for example, calling resume_run on a run that isn't paused returns run is not paused.
Unexpected server faults are masked to internal error, exactly as over REST, and a write tool called
with a read-only key returns this tool requires a full-access (secret) API key.