Approvals
Developer·3 minutes to read
Verified against docs/superpowers/specs/2026-09-15-p4-a-typed-http-api-openapi-webhooks.md §4.3, §4.4, §5.2, §6; re-verified against the code at close-out.
What it does
When a bot wants to do something the policy does not release on its own, it raises an approval and waits: these endpoints list what is pending, read one, and answer it (F-APR-1 to F-APR-7 — the approval card's fixed resource field, the deterministic policy engine, taint, saved rules, the automatic reviewer, human-only writes, and signed approvals).
Authentication
An organisation API key. GET /approvals and GET /approvals/{id} need scope read or wider; POST /approvals/{id}/decide needs write or wider. The decision is recorded as the admin the key is bound to — a script is that person's instrument, and nothing on this surface lets a bot answer its own approval. See Authentication and keys.
Request
GET /approvals
<!-- generated:begin GET /approvals --> <!-- generated:end GET /approvals -->GET /approvals/{id}
<!-- generated:begin GET /approvals/{id} --> <!-- generated:end GET /approvals/{id} -->POST /approvals/{id}/decide
<!-- generated:begin POST /approvals/{id}/decide --> <!-- generated:end POST /approvals/{id}/decide -->GET /approvals takes state=pending. A decision is approved or denied and nothing else: the app's third answer, the standing always allow, is refused at the schema here, because a saved rule is policy and policy is written in the app where a person can see what they are signing up to.
curl -sS "$BOTSEON_API_ORIGIN/api/v1/approvals?state=pending&limit=20" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "accept: application/json"
const res = await fetch(
`${process.env.BOTSEON_API_ORIGIN}/api/v1/approvals?state=pending&limit=20`,
{
headers: {
authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
accept: 'application/json',
},
},
);
if (!res.ok) throw new Error(`approvals: ${res.status}`);
const page = (await res.json()) as {
data: Array<{ id: string; runId: string; toolName: string; resource: string | null }>;
nextCursor: string | null;
};
for (const approval of page.data) console.log(approval.toolName, approval.resource ?? '—');
Response
{
"id": "4a5b6c7d-8e9f-4a0b-8c1d-2e3f4a5b6c7d",
"runId": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
"toolName": "send_email",
"resource": "mail:ops@example.test",
"tags": ["external_send"],
"reason": null,
"inputRedacted": { "to": "ops@example.test", "subject": "Weekly digest" },
"decision": null,
"reviewVerdict": null,
"expiresAt": "2026-09-18T09:00:00.000Z",
"createdAt": "2026-09-18T08:00:00.000Z"
}
tags are the permission tags the policy engine matched on — external_send is one of the eleven, and one of the set that needs an approval unless a rule says otherwise. resource is a fixed field the model does not author, which is what makes it safe to read in a script. inputRedacted is the app's own redacted projection of the tool input; this API adds no unredacted reader. reviewVerdict is the automatic reviewer's reading — allow, block or unclear — and it is an input to the decision, never the authority for it.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below the scope the route needs. | Use a write key to decide. |
| 400 | invalid_request | The decision is not approved or denied, or the POST carries no Idempotency-Key. | Answer with one of the two values. |
| 404 | not_found | No such approval in this organisation, or it belongs to a run this person cannot see. | List the pending ones first. |
| 409 | conflict | The approval has expired, or it has already been decided. | Read it again; a decided approval keeps its answer. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
Two one-minute request buckets, 300 per key and 1 000 per organisation. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.
Last verified against build c0f77aa.