Approvals API

Decide the runs waiting on a person from your own tooling: list open approvals and approve, deny, or approve with edits over HTTP.

An approval is a step that parks its run in needs_attention until someone approves or denies it; the decision resumes the run from its checkpoint. Reads work with a public key; the decision needs a secret key.

Endpoints

Method + pathPurpose
GET /approvalsThe project's approvals, newest first.
GET /approvals/{id}One approval, with the proposed tool call and its context.
POST /approvals/{id}/decisionApprove or deny an open approval; the parked run resumes.

Listing

GET /approvals accepts:

ParamMeaningDefault
statusOne of pending, escalated, approved, denied, cancelled. pending and escalated are open, awaiting a decision. An unknown value returns 400.all
runIdOnly one run's approvals.all
limitPage size, 1-1000. A non-integer returns 400.100

A decided approval stays listable as its audit trail. Each approval is:

{
  "id": "01JZR4A0...", "runId": "01JZR3Z9...",
  "workflow": "support.refund", "app": "support", "step": "refund-gate",
  "tool": "issue-refund",
  "args": { "orderId": "A1", "amount": 4200, "currency": "usd" },
  "risk": "high",
  "policy": "tools.issue-refund -> require approval",
  "summary": "Refund 4200 to A1 for a duplicate charge",
  "escalatesTo": "#support-leads",
  "onTimeout": "escalate",
  "status": "pending",
  "requestedAt": "2026-07-01T10:00:00Z",
  "expiresAt": "2026-07-01T10:30:00Z"
}
FieldMeaning
id, runIdThe approval and the run parked on it.
workflow, appJoined from the parked run.
stepThe step.approval name in the workflow.
tool, argsThe proposed action awaiting sign-off.
riskThe declared risk level.
policy, summary, contextAnnotations for whoever reviews it. Absent when not set.
escalatesToThe escalation target once expiresAt passes, on the default onTimeout: "escalate".
onTimeoutWhat the deadline does to this approval undecided: escalate (the default), approve, reject, or fail. See Timeouts.
statuspending, escalated, approved, denied, or cancelled. cancelled is terminal with no decision on record: the run went terminal under it, or its onTimeout was fail.
requestedAt, expiresAtWhen it was requested, and when its onTimeout action fires. expiresAt absent without a timeout.
decidedAt, decidedBy, editedArgsSet once decided: when, by whom, and the decider's edited arguments (approve-with-edits). Absent while open.
decidedViaWhich surface the decision arrived through. See Deciding.

Deciding

POST /approvals/{id}/decision applies the decision and resumes the run:

{ "status": "approved", "args": { "orderId": "A1", "amount": 2100, "currency": "usd" } }

status must be approved or denied. args, when present, replaces the proposed tool arguments on the approved decision - approve-with-edits; the workflow receives them as the effective decision.args. The response is the decided approval.

The decider is recorded from the authenticated caller: the X-Duraton-Actor header when a platform-issued key supplies one, otherwise the API key's name. Scope does not grant that - a customer key is full-scope too, and one that could name a person would be signing their name to its own actions. The request body cannot set it, so a decision can never be attributed to someone who did not make it, and the approval record always agrees with the audit log.

decidedVia records which surface the decision arrived through, alongside who made it. It is derived the same way - from how the request authenticated - and is likewise not settable by the body, because the surfaces do not carry the same weight:

decidedViaRecorded whenWhat it evidences
consoleA platform key naming the acting personA signed-in person decided
mcpThe request reached /mcpAn agent decided with a write tool
apiAny other authenticated callA credential decided; nobody is named
timeoutThe approval's own onTimeout resolved itNobody decided; the deadline did
unrecordedDecided before this field existed, or with auth disabledNothing is claimed

An auditor asking how a high-risk tool call was cleared needs that difference: the same person clearing a gate from a signed-in session and from a raw API call leaves the same decidedBy. A timeout resolution has no caller to derive either field from, so it records decidedBy: "system:timeout" alongside decidedVia: "timeout" rather than an empty decider that would read as an unattributed person.

import { createClient } from "@duraton/sdk/client";

const duraton = createClient({ url: process.env.DURATON_URL! });
const open = await duraton.approvals.list({ status: "pending" });
await duraton.approvals.decide(open[0].id, { status: "approved" });

Error codes

StatusWhen
400An unknown status filter; a decision whose status is not approved/denied; or invalid args JSON.
404The approval id does not exist.
409Deciding an approval that is no longer open: already decided, already resolved by its onTimeout, or cancelled.

On this page