Skip to content
/docs

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

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesAPI 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

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesAPI key or Bearer JWT
Content-TypeYesapplication/json

Body

{
  "decisionId": "d7f2a1b3-4c5e-6f78-9a0b-1c2d3e4f5a6b",
  "toolName": "transfer_funds",
  "arguments": { "amount": 5000, "to": "vendor-123" },
  "expiresInSeconds": 3600
}
FieldTypeRequiredDescription
decisionIdstring (UUID)NoLink to an existing decision record
toolNamestringYesName of the tool awaiting approval
argumentsobjectNoTool call arguments for reviewer context
expiresInSecondsnumberNoTTL 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

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesAPI key or Bearer JWT

Path parameters

ParameterTypeDescription
idstringThe 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

FieldTypeDescription
idstringApproval record ID
status"pending" | "approved" | "denied" | "expired"Current status
toolNamestringName of the tool awaiting approval
argumentsobject?Tool call arguments (included while pending)
resolvedBystring?Who resolved the approval
resolvedAtstring?ISO 8601 timestamp of resolution
createdAtstring?ISO 8601 timestamp of creation
expiresAtstring?ISO 8601 timestamp of expiration

POST /v1/approvals/:id/resolve

Resolve a pending approval, approve or deny.

Headers

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesAPI key or Bearer JWT
Content-TypeYesapplication/json

Path parameters

ParameterTypeDescription
idstringThe approval ID to resolve

Body

{
  "action": "approve",
  "resolvedBy": "user@company.com"
}
FieldTypeRequiredDescription
action"approve" | "deny"YesResolution action
resolvedBystringYesIdentifier of the person resolving

Response

{
  "id": "apr_abc123def456",
  "status": "approved",
  "resolvedBy": "user@company.com",
  "resolvedAt": "2025-01-15T10:31:15Z"
}

Errors

StatusCodeDescription
404not_foundApproval ID does not exist
410expiredApproval has already expired
400invalid_statusApproval 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:

  1. The original require_approval decision writes a receipt when the approval is created.
  2. Resolving the approval appends a chained receipt with decision: "approval_approved" or decision: "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:

  1. Validate endpoint returns { decision: "require_approval", approval_id: "apr_..." }
  2. SDK fires the onApprovalRequired / on_approval_required hook
  3. SDK polls GET /v1/approvals/:id every 2 seconds (configurable)
  4. When status changes from pending, SDK returns the decision to the agent
  5. If the poll exceeds the timeout (default 5 minutes), SDK throws ApprovalTimeoutError
SDK approval polling flow: agent calls tool, SDK validates with cloud, allow executes, deny throws error, require approval fires hook then polls GET /v1/approvals/:id until approved, denied, or expired