Skip to Content
API referenceApprovals API

Approvals API

Two surfaces:

  • GET /v1/approvals/:id — agent-facing status poll. Auth: agent JWT.
  • POST /v1/approvals/:id/decide — owner-facing decision. Auth: Clerk JWT.

When POST /v1/actions returns 202 pending_approval, the agent polls the status endpoint. The owner decides via the dashboard, mobile passkey (see Passkeys), or magic-link email (delegates).

GET /v1/approvals/:id

GET /v1/approvals/01HKZ7... HTTP/1.1 Host: api.agentvalet.ai Authorization: Bearer <agent-rs256-jwt>

Response

{ "approval_id": "01HKZ7...", "status": "approved", "result": { "status": 200, "data": { "ok": true } }, "execution_error": null, "expires_at": "2026-05-19T08:00:00Z", "executed_at": "2026-05-18T10:42:11Z", "created_at": "2026-05-18T10:40:55Z" }
FieldNotes
statuspending / approved / denied / expired
resultPresent when status === "approved" AND the post-approval re-execution succeeded. Mirrors the POST /v1/actions success envelope
execution_errorPresent when the post-approval re-execution failed (e.g. credential rotated since the request was queued). String.
expires_at24 hours from created_at
executed_atWhen the post-approval re-execution ran, or null if not yet

If the agent calls this endpoint while status is still approved but executed_at is null, the proxy triggers re-execution synchronously before returning. So a single successful poll is enough — the agent doesn’t need to differentiate “approved but not run” from “approved and run.”

Errors

StatusCause
401Missing or invalid agent JWT
403The approval belongs to a different agent / different org
404No approval with that id
429Rate limit (60/min per agent)

POST /v1/approvals/:id/decide

Owner-facing. The dashboard, mobile shell, and approval magic-link page all call this.

POST /v1/approvals/01HKZ7.../decide HTTP/1.1 Host: api.agentvalet.ai Authorization: Bearer <clerk-jwt> Content-Type: application/json { "decision": "approved" }

Response

{ "status": "approved", "executed_at": "2026-05-18T10:42:11Z" }

After updating the row, the proxy triggers re-execution of the queued action so the agent’s next poll sees a complete result. If the re-execution fails (e.g. the agent has been suspended in the interim), execution_error is recorded on the approval row and exposed via the status poll.

Errors

StatusCause
400decision not approved or denied
401No Clerk JWT
403The approval doesn’t belong to your org
404No approval with that id
409Already decided — approvals are single-action
410The approval has expired (24h window)

Approval delegates configured in Settings → Approval Delegates receive emails with a single-use magic-link token. Following the link lands at https://app.agentvalet.ai/approve?token=<jwt> and uses a separate delegate-only endpoint — not decide. Delegates do not need a Clerk account.

The token is one-time-use: once approved or denied, the same link will return 410. Tokens expire in 24 hours, mirroring the approval window.

Lifecycle

request hits /v1/actions with require_approval=true → approval_queue row created (status=pending, expires_at = now + 24h) → owner notified (push, email to delegates) → owner decides (dashboard / mobile passkey / magic link) → status set to approved/denied → if approved: reExecuteApproval() runs the original call, persists result_json or execution_error → agent polls GET /v1/approvals/:id → returns final status + result (or execution_error) → 24h timer: cron expires rows still pending → status=expired

See How pending approvals work for the dashboard-side narrative.

Last updated on