> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/ai/approvals) 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 + path                   | Purpose                                                    |
| ------------------------------- | ---------------------------------------------------------- |
| `GET /approvals`                | The project's approvals, newest first.                     |
| `GET /approvals/{id}`           | One approval, with the proposed tool call and its context. |
| `POST /approvals/{id}/decision` | Approve or deny an open approval; the parked run resumes.  |

## Listing

`GET /approvals` accepts:

| Param    | Meaning                                                                                                                                                    | Default |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `status` | One of `pending`, `escalated`, `approved`, `denied`, `cancelled`. `pending` and `escalated` are open, awaiting a decision. An unknown value returns `400`. | all     |
| `runId`  | Only one run's approvals.                                                                                                                                  | all     |
| `limit`  | Page size, `1`-`1000`. A non-integer returns `400`.                                                                                                        | `100`   |

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

```json theme={null}
{
  "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"
}
```

| Field                                  | Meaning                                                                                                                                                                          |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`, `runId`                          | The approval and the run parked on it.                                                                                                                                           |
| `workflow`, `app`                      | Joined from the parked run.                                                                                                                                                      |
| `step`                                 | The `step.approval` name in the workflow.                                                                                                                                        |
| `tool`, `args`                         | The proposed action awaiting sign-off.                                                                                                                                           |
| `risk`                                 | The declared risk level.                                                                                                                                                         |
| `policy`, `summary`, `context`         | Annotations for whoever reviews it. Absent when not set.                                                                                                                         |
| `escalatesTo`                          | The escalation target once `expiresAt` passes, on the default `onTimeout: "escalate"`.                                                                                           |
| `onTimeout`                            | What the deadline does to this approval undecided: `escalate` (the default), `approve`, `reject`, or `fail`. See [Timeouts](/ai/approvals#timeouts-and-escalation).              |
| `status`                               | `pending`, `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`, `expiresAt`             | When it was requested, and when its `onTimeout` action fires. `expiresAt` absent without a timeout.                                                                              |
| `decidedAt`, `decidedBy`, `editedArgs` | Set once decided: when, by whom, and the decider's edited arguments (approve-with-edits). Absent while open.                                                                     |
| `decidedVia`                           | Which surface the decision arrived through. See [Deciding](#deciding).                                                                                                           |

## Deciding

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

```json theme={null}
{ "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:

| `decidedVia` | Recorded when                              | What it evidences                     |
| ------------ | ------------------------------------------ | ------------------------------------- |
| `console`    | A platform key naming the acting person    | A signed-in person decided            |
| `mcp`        | The request reached `/mcp`                 | An agent decided with a write tool    |
| `api`        | Any other authenticated call               | A credential decided; nobody is named |
| `timeout`    | The approval's own `onTimeout` resolved it | Nobody decided; the deadline did      |
| `unrecorded` | Decided before key attribution existed     | Nothing 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](/ai/approvals#timeouts-and-escalation) 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.

<Tabs>
  <Tab title="TypeScript">
    ```ts theme={null}
    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" });
    ```
  </Tab>

  <Tab title="REST API">
    ```sh theme={null}
    curl "$DURATON_URL/approvals?status=pending"
    curl -X POST "$DURATON_URL/approvals/01JZR4A0.../decision" -d '{"status":"approved"}'
    curl -X POST "$DURATON_URL/approvals/01JZR4A0.../decision" -d '{"status":"denied"}'
    ```
  </Tab>
</Tabs>

## Error codes

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