Requests
Submit and poll approval requests through the agent API.
Requests are the core of the agent integration. This page covers the
request procedures. All examples assume the API at
https://af-api.ivalt.com; see Connections for auth.
submitRequest
Creates an approval request in pending state and routes it to a
reviewer according to your routing configuration.
curl -s https://af-api.ivalt.com/api/v1/ai-agent/submitRequest \
-H 'x-ai-agent-key: <agent-key>' \
-H 'content-type: application/json' \
-d '{
"json": {
"externalRequestId": "REQ-1001",
"idempotencyKey": "0f2a5f18-2f4c-4f8f-9c77-3b4f4c5f9a11",
"title": "On-behalf-of correspondence",
"actionType": "send_mail",
"actionSummary": "Agent drafted mail on behalf of insured",
"displaySnapshot": { "recipient": "vendor@example.com", "amount": 4500 },
"contentHash": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"expiresAt": "2026-11-17T22:00:00.000Z",
"riskLevel": "high"
}
}'| Field | Type | Required | Description |
|---|---|---|---|
externalRequestId | string | Yes | Your unique ID for the request; unique per connection |
idempotencyKey | string | Yes | Client-generated key (UUID recommended); unique per connection |
recipientUserId | string | No | Send the request directly to this workspace user (must be an active or invited approver); bypasses routing |
title | string | Yes | Display title shown to the reviewer |
actionType | string | Yes | Action category |
actionSummary | string | Yes | Short human-readable summary |
displaySnapshot | object | Yes | Arbitrary JSON rendered for the reviewer; title, actionType, actionSummary are merged into it |
contentHash | string | Yes | Hash of the full action content; stored as the receipt's requestHash |
expiresAt | ISO 8601 datetime | Yes | When the request expires if undecided |
riskLevel | enum | No | low | medium | high | critical; defaults to medium |
The response is the full request object (status: "pending",
version: 1, id like apr_…). Store id — you need it for every
subsequent call.
Default routing and recipient routing
Every workspace has a built-in default approval path that routes to the
workspace's approver role, so agents can submit before any routing is
configured.
Pass recipientUserId to send the request straight to a specific
approver — the user ID as it appears under People & roles in the
console. If the user isn't an active or invited member of the
workspace, the call fails with bad_request.
Errors
| Condition | Error |
|---|---|
| Missing/invalid fields | validation_error (400) |
Missing or unknown x-ai-agent-key | unauthorized (401) |
No assignee resolvable — routing unconfigured, or recipientUserId isn't an active/invited workspace user | bad_request (400) |
Idempotency key or externalRequestId already used | idempotency_conflict (409) |
Idempotency
Uniqueness is enforced per connection on both externalRequestId and
idempotencyKey. Retrying with the same idempotencyKey returns the
originally created request, so submitRequest is safe to retry on
timeouts — generate the key once per logical request, not once per
attempt.
Edge case: if routing resolution fails after the request is accepted,
submitRequest can return bad_request even though the request
exists without an assignee — and an idempotent replay returns that
unassigned request rather than an error. Treat a bad_request from
submitRequest as possibly having persisted the request: follow up
with getStatus on the same idempotencyKey/externalRequestId
before resubmitting with a new key.
getStatus
Returns the request plus delivery-aware status. This is the polling entry point.
curl -s https://af-api.ivalt.com/api/v1/ai-agent/getStatus \
-H 'x-ai-agent-key: <agent-key>' \
-H 'content-type: application/json' \
-d '{"json": {"requestId": "apr_1234_v1"}}'{
"json": {
"request": { "id": "apr_1234_v1", "status": "pending", "version": 1, "...": "..." },
"isFinal": false,
"deliveryModes": ["polling", "webhook"],
"reviewProgress": {
"assigned": 1,
"completed": 0,
"requiredApprovals": 1,
"decisions": []
},
"receiptId": null,
"nextPollAfterMs": 5000
}
}| Field | Meaning |
|---|---|
request | The full request object; status is one of pending, opened, decision_in_progress, verification_required, approved, rejected, expired, failed |
isFinal | true once the status is approved, rejected, or expired |
deliveryModes | Which delivery modes apply to this request: polling (always), plus webhook when configured |
reviewProgress | Reviewer progress for the request. Each request currently has a single approver |
receiptId | The proof/receipt ID once a decision is finalized, else null |
nextPollAfterMs | Minimum delay before polling again. Wait at least this long between calls; do not hard-code a tighter interval |
getRequest
Returns the raw request object only — same shape as the
submitRequest response, without the status envelope. Use it when
you don't need isFinal/reviewProgress/deliveryModes.
curl -s https://af-api.ivalt.com/api/v1/ai-agent/getRequest \
-H 'x-ai-agent-key: <agent-key>' \
-H 'content-type: application/json' \
-d '{"json": {"requestId": "apr_1234_v1"}}'Polling recipe
import time, requests
def wait_for_decision(api, key, request_id):
while True:
r = requests.post(
f"{api}/api/v1/ai-agent/getStatus",
headers={"x-ai-agent-key": key},
json={"json": {"requestId": request_id}},
)
status = r.json()["json"]
if status["isFinal"]:
return status
time.sleep(status["nextPollAfterMs"] / 1000)When isFinal becomes true, check request.status: only
approved authorizes the protected action, and only after you verify
the signed receipt — see Receipts.