Docs · preview
API
Base URL https://api.enruta.ai/v0. JSON in, JSON out. One endpoint matters: request payment.
Authentication
Authorization: Bearer <key>. Keys belong to an organization and carry a role. Agent keys can create payment requests and read the records they produced. Developer keys can manage policies, rails, webhooks and read everything. Auditor keys can read and export records only.
Idempotency
POST /payment-requests requires an Idempotency-Key header. A repeated key returns the original response with decision.reasons including idempotent_replay. Keys live 24 hours.
Payment requests
Ask to pay. Returns the decision, and the mandate and credential on allow. Requires Idempotency-Key.
curl https://api.enruta.ai/v0/payment-requests \
-H "Authorization: Bearer $ENRUTA_AGENT_KEY" \
-H "Idempotency-Key: a3c9…" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agt_procurement_02",
"payee": { "name": "Demo Store", "domain": "store.enruta.ai" },
"amount": { "value": "168.40", "currency": "USD" },
"purpose": "20 reams A4 paper, 1 stapler",
"checkout": { "protocol": "ucp", "reference": "chk_8f3…" },
"evidence": [{ "type": "quote", "ref": "https://store.enruta.ai/q/1042", "digest": "sha256:…" }]
}'import { Enruta } from "@enruta/sdk";
const enruta = new Enruta(process.env.ENRUTA_AGENT_KEY!);
const r = await enruta.paymentRequests.create({
agentId: "agt_procurement_02",
payee: { name: "Demo Store", domain: "store.enruta.ai" },
amount: { value: "168.40", currency: "USD" },
purpose: "20 reams A4 paper, 1 stapler",
checkout: { protocol: "ucp", reference: "chk_8f3…" },
}, { idempotencyKey: "a3c9…" });
if (r.decision.result === "allow") pay(r.mandate!.credential);201 Created
{
"id": "preq_01J9…",
"decision": { "result": "step_up", "reasons": ["approval_required"], "policy": { "id": "pol_office_supplies", "version": 7 } },
"approval": { "id": "apr_…", "url": "https://app.enruta.ai/a/apr_…", "expires_at": "2026-11-30T18:16:45Z" },
"mandate": null,
"record_id": "rec_01J9…"
}On allow, mandate carries id, amount, expires_at and credential: the reference the payee’s payment handler accepts (a network token or a single-use card reference, never a card number). trace is optional: a digest of the agent’s own reasoning or run log, so the record can prove what the agent knew without Enruta storing it.
The request, its current decision, and the approval and mandate state. Poll this after a step_up, or subscribe to the webhook.
Approvals
Approvals are completed by a person, in the console or through the signed link. The API exposes state only.
{ "id": "apr_…", "state": "pending" | "approved" | "declined" | "expired",
"actor": "usr_…", "method": "passkey", "completed_at": "…" }Mandates
One mandate: scope, expiry, consumed state, the request hash it binds.
Filter with agent_id and state (issued, consumed, expired).
Records
The record. Add /bundle for its attestation bundle.
Filter with agent_id, from, to. Paginated with cursor.
{ "from": "2026-11-01", "to": "2026-11-30" }
→ 202 { "id": "exp_…" }
GET /v0/exports/{id} → a signed download URL when readyAn export is a directory: one .json per record, one .bundle.json beside it, the verifier, the schema and the test vectors.
Policies and rails
Policies are created and signed in the console; the API reads them (GET /v0/policies, GET /v0/policies/{id}/versions/{n}). Rail connections are created by an administrator in the console with credentials scoped on the rail’s side; the API lists them (GET /v0/rails).
Webhooks
| Event | When |
|---|---|
payment_request.decided | A decision was made |
approval.completed | A person approved or declined |
mandate.issued | A mandate exists and can be redeemed |
mandate.consumed | The rail authorized against the mandate |
record.sealed | The record was sealed and time-stamped |
Deliveries carry an Enruta-Signature header: HMAC-SHA256 over the timestamp and body with your endpoint secret. Retries follow an exponential schedule for 24 hours.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed |
| 401 | unauthenticated | No key, or an unknown key |
| 403 | forbidden | The key’s role cannot do this |
| 404 | not_found | No such object in this organization |
| 409 | idempotency_conflict | Same key, different body |
| 429 | rate_limited | Slow down; Retry-After is set |
A denied payment is not an error. It is a 201 with decision.result: "deny", and it produces a record.