Approvals API
Endpoints for human-in-the-loop approval workflows, list, create, poll, and resolve approvals.
The approvals API powers Veto's human-in-the-loop (HITL) workflow. When a policy requires human review, an approval record is created and the SDK polls until a human resolves it.
HITL requires the Team plan or above.
GET /v1/approvals/pending
List all pending approvals for the current organization.
Headers
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | API key or Bearer JWT |
Response
{
"data": [
{
"id": "apr_abc123def456",
"toolName": "transfer_funds",
"arguments": { "amount": 5000, "to": "vendor-123" },
"status": "pending",
"expiresAt": "2025-01-15T10:35:00Z",
"createdAt": "2025-01-15T10:30:00Z"
}
]
}POST /v1/approvals
Create an approval request manually. Useful for triggering HITL outside the normal validation flow.
Headers
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | API key or Bearer JWT |
Content-Type | Yes | application/json |
Body
{
"decisionId": "d7f2a1b3-4c5e-6f78-9a0b-1c2d3e4f5a6b",
"toolName": "transfer_funds",
"arguments": { "amount": 5000, "to": "vendor-123" },
"expiresInSeconds": 3600
}| Field | Type | Required | Description |
|---|---|---|---|
decisionId | string (UUID) | No | Link to an existing decision record |
toolName | string | Yes | Name of the tool awaiting approval |
arguments | object | No | Tool call arguments for reviewer context |
expiresInSeconds | number | No | TTL in seconds (60–86400, default: 3600) |
Response (201)
{
"id": "apr_abc123def456",
"toolName": "transfer_funds",
"status": "pending",
"expiresAt": "2025-01-15T11:30:00Z",
"createdAt": "2025-01-15T10:30:00Z"
}GET /v1/approvals/:id
Poll an approval record. The SDK calls this automatically, you do not need to call it directly.
Headers
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | API key or Bearer JWT |
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | The approval ID |
Response
Pending
{
"id": "apr_abc123def456",
"status": "pending",
"toolName": "transfer_funds",
"arguments": { "amount": 5000, "to": "vendor-123" },
"createdAt": "2025-01-15T10:30:00Z",
"expiresAt": "2025-01-15T10:35:00Z"
}Approved
{
"id": "apr_abc123def456",
"status": "approved",
"toolName": "transfer_funds",
"resolvedBy": "user@company.com",
"resolvedAt": "2025-01-15T10:31:15Z"
}Denied
{
"id": "apr_abc123def456",
"status": "denied",
"toolName": "transfer_funds",
"resolvedBy": "user@company.com",
"resolvedAt": "2025-01-15T10:31:15Z"
}Expired
{
"id": "apr_abc123def456",
"status": "expired",
"toolName": "transfer_funds",
"expiresAt": "2025-01-15T10:35:00Z"
}Response fields
| Field | Type | Description |
|---|---|---|
id | string | Approval record ID |
status | "pending" | "approved" | "denied" | "expired" | Current status |
toolName | string | Name of the tool awaiting approval |
arguments | object? | Tool call arguments (included while pending) |
resolvedBy | string? | Who resolved the approval |
resolvedAt | string? | ISO 8601 timestamp of resolution |
createdAt | string? | ISO 8601 timestamp of creation |
expiresAt | string? | ISO 8601 timestamp of expiration |
POST /v1/approvals/:id/resolve
Resolve a pending approval, approve or deny.
Headers
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | API key or Bearer JWT |
Content-Type | Yes | application/json |
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string | The approval ID to resolve |
Body
{
"action": "approve",
"resolvedBy": "user@company.com"
}| Field | Type | Required | Description |
|---|---|---|---|
action | "approve" | "deny" | Yes | Resolution action |
resolvedBy | string | Yes | Identifier of the person resolving |
Response
{
"id": "apr_abc123def456",
"status": "approved",
"resolvedBy": "user@company.com",
"resolvedAt": "2025-01-15T10:31:15Z"
}Errors
| Status | Code | Description |
|---|---|---|
| 404 | not_found | Approval ID does not exist |
| 410 | expired | Approval has already expired |
| 400 | invalid_status | Approval is already resolved |
Approval receipts
A human-in-the-loop decision produces two linked veto.receipt/1 receipts, so the audit chain captures who escalated and who resolved:
- The original
require_approvaldecision writes a receipt when the approval is created. - Resolving the approval appends a chained receipt with
decision: "approval_approved"ordecision: "approval_denied".
The resolution receipt carries the same decision_id, the approval_id, and an approval_hash that binds the approval ID, the action, and resolvedBy. Because it links to the previous receipt via previous_receipt_hash, an exported chain proves the escalation and its resolution are tamper-evidently connected. Resolution failures to write a receipt are logged and never block the approval response.
Fetch the canonical payloads with GET /v1/decisions/:id/receipt or export them with GET /v1/receipts/export.
SDK behavior
The SDK handles polling automatically. The flow:
- Validate endpoint returns
{ decision: "require_approval", approval_id: "apr_..." } - SDK fires the
onApprovalRequired/on_approval_requiredhook - SDK polls
GET /v1/approvals/:idevery 2 seconds (configurable) - When status changes from
pending, SDK returns the decision to the agent - If the poll exceeds the timeout (default 5 minutes), SDK throws
ApprovalTimeoutError

