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"
}| Field | Notes |
|---|---|
status | pending / approved / denied / expired |
result | Present when status === "approved" AND the post-approval re-execution succeeded. Mirrors the POST /v1/actions success envelope |
execution_error | Present when the post-approval re-execution failed (e.g. credential rotated since the request was queued). String. |
expires_at | 24 hours from created_at |
executed_at | When 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
| Status | Cause |
|---|---|
| 401 | Missing or invalid agent JWT |
| 403 | The approval belongs to a different agent / different org |
| 404 | No approval with that id |
| 429 | Rate 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
| Status | Cause |
|---|---|
| 400 | decision not approved or denied |
| 401 | No Clerk JWT |
| 403 | The approval doesn’t belong to your org |
| 404 | No approval with that id |
| 409 | Already decided — approvals are single-action |
| 410 | The approval has expired (24h window) |
Delegate approvals (magic link)
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=expiredSee How pending approvals work for the dashboard-side narrative.