Docs Reference

API

LiveUpdated

View as Markdown

Base URL https://api.enruta.ai/v0. JSON in, JSON out. One endpoint starts everything: request payment. The rest reads what it produced, or reverses, settles, reconciles and disputes it.

Authentication

Authorization: Bearer <key>. Keys belong to an organization and carry a role. Agent keys (enruta_agt_…) create payment requests and reversals and read what they produced. Developer keys (enruta_dev_…) manage policies, rails, webhooks, consents, attestations and agents, and read everything. Auditor keys (enruta_aud_…) read records, reconciliation and disputes and export. Seller keys (enruta_slr_…) act for a seller organization’s verified domains only. Platform keys (enruta_plt_…) reach /v0/platform/* and, with the Enruta-Account header, act for one connected account (Platform accounts). Catalog keys (enruta_cat_…) write and read the account’s catalog through the Catalog API (/v0/catalog) and reach nothing else; an account that uses Discovery makes them in Settings › Developers › API keys, and they are the only keys of an account that uses Discovery alone. The server stores a key’s SHA-256 and an eight-character prefix, never the key.

Idempotency and paging

POST /v0/payment-requests requires an Idempotency-Key header. A repeated key returns the original response with decision.reasons including idempotent_replay; the same key with a different body is 409. Keys live 24 hours. A decision that fails inside Enruta after it has started (the database, the key store, a rail connection missing its stored credentials) is rolled back and nothing is sealed, so the same key decides again. A new request for the same payment takes a new key; decision.retryable and decision.retry_after say whether one may follow (retrying). Every list endpoint pages with cursor and limit.

Payment requests

POST/v0/payment-requests

Ask to pay. Returns the decision, and the mandate and credential on allow or observe. 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": "Ream & Rule", "domain": "reamandrule.com" },
    "amount": { "value": "24.98", "currency": "USD" },
    "purpose": "2 packs of ballpoint pens for the office",
    "checkout": { "protocol": "ucp", "reference": "chk_01M2511XQ4N7B8T2R6H9K3W5MD" },
    "evidence": [{ "type": "checkout", "ref": "https://reamandrule.com/checkout-sessions/chk_01M2511XQ4N7B8T2R6H9K3W5MD", "digest": "sha256:4b1c…" }],
    "intent": { "text": "Order two packs of pens, keep it under $30", "slots": { "quantity": 2, "max_total": "30.00" } },
    "catalog_snapshot_hash": "sha256:0d3e…"
  }'

Body fields added since v0: intent { text?, slots? } (the 0.2 intent_text, constraints and intent_mapping are still accepted), catalog_snapshot_hash?, policy_snapshot_hash?, deviations?. The caller’s IP is captured from the request as agent_ip. On clarify the response carries state: awaiting_clarification, mandate: null and decision.questions[]; on step_up it carries mandate: null and approval { id, url, expires_at }; on observe it carries the mandate and decision.shadow_result. Every decision carries retryable and retry_after (retrying), and a mandate carries retry_policy, visibility_scope and hidden_constraints_digest (null when the policy sets none), never the values it keeps from the merchant. complete_checkout: true (UCP checkouts only) asks Enruta to execute the checkout itself and adds order and completion to the response. When the rail cannot issue a credential for an allowed payment, the response carries state: handed_off, mandate: null and handoff { rail, reason, message, next_step } instead, and with complete_checkout a failed completion whose error says the payment was handed off and why (see “When the rail cannot issue” below).

GET/v0/payment-requests/{id}

The request, its current decision (with retryable and retry_after), the approval and mandate state, order and completion when Enruta placed the order, outcome, handoff, and since this release risk, settlement, reversals[], reconciliation, disputes[] and fulfillment summaries. state is one of decided, awaiting_clarification, awaiting_approval, approved, handed_off, declined, expired, executed, completed. Poll this after a step_up (?wait=25 holds the request open until something changes), or subscribe to the webhook.

POST/v0/payment-requests/{id}/handoff
{ "continue_url": "https://shop.example/cart/c/…",
  "order": { "id": "gid://shopify/Order/…", "status": "completed", "permalink_url": "https://…", "total": { "amount": "699.95", "currency": "USD" } } }

For merchants that complete checkout only on their own page: the checkout was handed to a person at the merchant’s continue_url. Without order: the request moves to handed_off, the mandate to void, the budget reservation is kept. With order (on the first call or a later one): the order becomes the receipt (self_asserted), outcome.status becomes completed, and the record seals. Repeats are idempotent. A request the rail handed off (below) has no checkout and no order to report: the call answers 409.

POST/v0/payment-requests/{id}/pay-resource
{ "url": "https://data.example/v1/prices?symbol=ACME",
  "method": "GET",
  "headers": { "Accept": "application/json" } }

200 {
  "paid": true,
  "request_id": "preq_01M3E6…", "mandate_id": "mnd_01M3E6…", "rail": "x402", "authorization_id": "x402_01M3E7…",
  "status": 200,
  "headers": { "content-type": "application/json", "payment-response": "eyJzdWNjZXNzIjp0cnVl…" },
  "body": "{ \"symbol\": \"ACME\", \"price\": \"41.20\" }",
  "truncated": false,
  "receipt": { "success": true, "transaction": "0x5d1c…", "network": "eip155:8453" },
  "record_id": "rec_01M3E6…",
  "answered_in_ms": 812
}

Pays an HTTP resource that answers 402 Payment Required (x402 or MPP) with the mandate of an allowed payment request, and returns what the resource answered. Only the request’s own agent key: a developer key gets 403, another agent’s key 404. The body is strict: url, method (GET when absent, or POST, PUT, PATCH, DELETE), up to 50 headers (none that Enruta or the transport sets, such as Host, Content-Length, Payment-Signature or X-Payment) and body, a string of up to 256 KB that a GET never carries. The URL is https, on the payee’s domain, and exactly the request’s checkout.reference when the request named one.

The request must hold an issued mandate on an x402 or mpp rail, or an MPP mandate whose payment channel is open. Enruta calls the resource, pays its 402 inside the mandate (never more, and only to the payee) and answers 200 with paid, the resource’s status, a safe subset of its headers (content, caching, redirects and payment receipts), its body as text up to 256 KB (truncated when cut) and the receipt (the x402 PAYMENT-RESPONSE or the MPP Payment-Receipt). A paid resource seals the payment record, and the settlement record once its stage is final. When the rail refuses, the answer is paid: false with reason (amount_exceeds_mandate, merchant_mismatch, rail_declined, rail_timeout, …) and detail, and the mandate stays usable. 400 for a URL off the payee or other than the one the request named, or a malformed body; 409 when no mandate can pay: the approval or the clarification is still open, the payment was handed off or denied, the mandate was used or has expired, or its rail pays no HTTP resources. MCP pay_http_resource; CLI enruta pay-resource.

POST/v0/payment-requests/{id}/clarifications
{ "answers": [{ "question_id": "q_max_total", "answer": "100.00" }], "answered_by": "user" }

Answers the questions of a clarify decision and decides the request again; returns the request. The request’s agent key or a developer key. answered_by is user unless the body says agent (intent and clarification).

GET/v0/payment-requests/{id}/economics

The money split of the payment: the flows and each party’s view, every line marked actual, estimate or plan, with its source and date. Agent keys read their own payments; developer and auditor keys any.

When the rail cannot issue

The policy allowed the payment, or a person approved it, but the rail issued no credential. The request moves to handed_off with mandate: null, handoff { continue_url, handed_off_at, order: null, rail, reason, message, next_step } and outcome { status: "handed_off", handed_off_at, rail, reason, message, next_step, continue_url? }. continue_url is the rail’s own page when the rail named one, otherwise the payment’s page in the console, under its account (https://app.enruta.ai/payments/{account}/payments/{id}). Nothing was paid, the budget reservation is released, the record seals at once with that outcome, an idempotent replay returns the same response, and payment_request.handed_off is sent. A rail hand-off is final: POST /v0/payment-requests/{id}/handoff answers 409, and with complete_checkout: true no order is placed (completion: { state: "failed", error: "Handed off to a person: the <rail> rail issued no credential (<reason>: <message>), so no order was placed" }).

reasonRailWhat it means
rail_limitLinkAbove the 500 USD a Link spend request can carry
seller_profile_missingLink, Stripe shared payment tokenNo Stripe profile is known for the seller
read_onlyBill.comThe connection reads bills, approvals and payments and approves nothing
bill_missing, untrusted_session, bill_archived, bill_decided, vendor_mismatch, amountBill.comNo bill id in the checkout, no MFA-trusted session, an archived or already decided bill, a bill from another vendor, or a bill above the mandate
recipient_missingMercuryNo active Mercury recipient for the payee
currencyMercury, Bill.com, x402The rail pays in another currency
not_configuredRamp, x402, MPPThe connection lacks the user, the signer or the payment method it needs
rail_unavailableEvery railThe rail failed while issuing: it answered 5xx, 408, 425 or 429, timed out, or could not be reached
rail_errorEvery railAny other failure while issuing, such as a 4xx refusal for bad credentials

A payment handed off with rail_unavailable or rail_error does not count toward the policy’s duplicate window, so the same payment can be requested again once the rail answers or the connection is fixed, and its decision answers retryable: true. Every other reason still counts: a person follows next_step, and retryable is false. Under a policy’s retry_policy, every hand-off counts as a failed attempt.

Merchants and agent identity

GET/v0/merchants/identity?domain=reamandrule.com

Checks a merchant before paying and returns the result with its domain: status (verified, unverified or mismatch), each check, the seller organization and when. Agent, developer and auditor keys (trust model).

POST/v0/attestations/nonce

A fresh nonce for an agent_key proof.

POST/v0/attestations

{ agent_id, anchor: { type, party?, …evidence } } verifies an identity anchor and seals a kya_attestation record. The v1 body (registry, registry_ref, method, evidence) is still accepted. GET /v0/attestations lists attestations with their type, tier and status.

GET/v0/agents/{id}/identity

The agent’s { assurance, anchors }.

Recurrences

POST/v0/recurrences

{ agent_id, payee, currency, per_occurrence_max, cumulative_max, amount_variance: { type, pct? }, schedule: { interval, anchor }, end_date?, consent_id?, purpose } creates a recurrence and seals its record. GET /v0/recurrences lists them.

GET/v0/recurrences/{id}

One recurrence. PATCH the same path to pause, resume or end it; every change is sealed. GET /v0/recurrences/{id}/occurrences lists the payments made under it.

Reversals

POST/v0/reversals
{ "record_id": "rec_01M25129GZMJXTNQ47TMZRDGHN",     // or request_id, or mandate_id
  "kind": "refund",                                   // void | cancel | refund | return | credit | price_adjustment | chargeback_credit
  "amount": { "value": "9.99", "currency": "USD" },  // default: the remaining reversible amount
  "reason_code": "customer_request",
  "refund_to": "original_form_of_payment" }             // or credit

201 { "reversal": { "id": "rev_01M2G8YV5R3T7W2Q9K4N8B6DXP", "stage": "settled", "status": "completed", "fund_change": true,
                    "mandate_effect": "refill_pending_settlement", "reversal_record_id": "rec_01M2G8Z0Q4W7T2K9N5B3D6H1XC", … },
      "decision": { "result": "allow", "checks": [{ "name": "original_amount", "ok": true }, { "name": "stage", "ok": true }, { "name": "finality", "ok": true }, { "name": "window", "ok": true }, { "name": "cancellable_until", "ok": true }] },
      "adjustments": { "ucp": { … }, "acp": { … }, "adyen": { … }, "shopify": { … } } }

A reversal is a decision (allow, step_up above the policy’s reversal.step_up_above, or deny), executed by stage: a void before authorization, a reversal of an uncaptured authorization, a refund after capture. Agent keys may reverse their own records; developer keys any. Field by field: Reversal.

GET/v0/reversals

Filter with record_id, status, from, to; paginated.

GET/v0/reversals/{id}

One reversal with its events[], decision, approval and the id of its sealed record.

GET/v0/reversals/{id}/adjustments?protocol=ucp

The reversal rendered as a merchant-side adjustment for ucp, acp, adyen or shopify (Adjustments).

Settlements

GET/v0/settlements/{record_id}

The settlement record of a payment: stage, amounts, finality_class, finality_at, rail references, fulfillment. Agent (own), developer and auditor keys.

Disputes

GET/v0/records/{id}/dispute-packet

Generates (or returns) the dispute packet for a sealed payment record: the packet, its signed JWS, the CE3.0 element checklist and the six exports. POST to the same path regenerates it. Developer and auditor keys.

GET/v0/records/{id}/representment.pdf

The one-page representment PDF, application/pdf.

GET/v0/disputes

Dispute packets and their outcomes. Filter with state, from, to. Agent keys see their own records’ packets.

POST/v0/disputes/{id}/outcome
{ "outcome": "won", "note": "Issuer accepted the mandate and login id", "submitted_to": "stripe" }

Records the outcome (won, lost, withdrawn), seals a dispute_outcome record chained to the packet, patches dispute_afterlife in the original record’s bundle, and emits dispute.outcome_recorded.

Reconciliation

GET/v0/reconciliation/lines?period=2026-W37&state=partial

Reconciliation lines for an ISO week; state filters by match_state. GET /v0/reconciliation/lines/{mandate_id} returns one line. Developer and auditor keys.

GET/v0/reconciliation/reports/{period}

The weekly report: counts by match state, exceptions with suggested actions, coverage, root hash, state draft or signed, the signer and the sealed record ids.

POST/v0/reconciliation/reports/{period}/build

Builds or rebuilds the draft for the period. The weekly job does this for the previous week every Monday at 01:00 UTC.

POST/v0/reconciliation/reports/{period}/sign

Signs the draft by link key over the API; a passkey signature is made in the console. Signing seals every line and then the report as linked records.

Consents and agents

POST/v0/consents

Creates a consent record for an agent; GET /v0/consents lists them and GET /v0/consents/{id} reads one. Developer keys.

POST/v0/consents/{id}/revoke
{ "effective_at": "2026-10-01T00:00:00Z", "cascades_scheduled": true }

Revokes a consent; every field is optional. effective_at is now when absent; a later time schedules the revocation and the consent stays active until then; a time more than a minute in the past is 400. Without cascades_scheduled new mandates stop and those already issued stay valid until they expire. With cascades_scheduled: true what the agent has scheduled stops too: recurrences made under the consent end, its unused mandates are voided, its pending approvals expire, and when no other consent covers the agent its other recurrences pause and the agent is paused. institution_notified_at records when a consumer told their institution. Answers 200 with the consent, its revocation { revoked_at, by, effect, institution_notified_at, effective_at, cascades_scheduled, notified_institution_at, cascade }, record_id and revoke_record_id. Idempotent: a revoked consent comes back as it is, and a scheduled revocation stands until it takes effect or one effective now brings it forward. Developer keys (consent record).

POST/v0/agents/{id}/pause

Pauses an agent with a reason; every later request is denied with agent_paused and the pause is a sealed record. POST /v0/agents/{id}/resume lifts it. Developer keys; the console has the same buttons.

Records, exports, sandbox

GET/v0/records

Filter with agent_id, from, to and, since this release, kind (payment, reversal, settlement_record, …). GET /v0/records/{id} serves payment and linked records alike; add /bundle for the bundle, /verify for a server-side verification report, /linked for the records chained to this one.

POST/v0/exports
{ "from": "2026-09-01", "to": "2026-09-30", "principal_login_id": "halvern/dana.reyes@halvernrobotics.com", "kinds": "all", "consents": true }
→ 202 { "id": "exp_01M2H4…" }
GET /v0/exports/{id} → a signed download URL when ready

An export is a zip: one .json per record with its .bundle.json, linked/ for the linked records in range, consents/ for the sealed consent records, the daily checkpoints, the verifier, the schema and a README with the verify command. The three new fields are optional; an export without them is exactly the v0 export. See Compliance export.

POST/v0/sandbox/keys

No authentication, five per IP per hour. Creates a sandbox organization with an agent key, a developer key, the sample policy and a claim URL a person can open later (quickstart).

Approvals and the organization

POST/v0/approval-delegations

Delegates approval to a member or an e-mail address within a scope { max_amount, payees?, agents?, until }. GET /v0/approval-delegations lists them; DELETE /v0/approval-delegations/{id} revokes one (Signed policy).

PATCH/v0/org/settings

slack_webhook_url (approval requests also post to Slack), psp_profile (the merchant’s PSP pricing, used by the money split) and sso. Developer keys or the console.

POST/v0/org/verification

Starts the organization’s verification: a DNS TXT record enruta-verification=<token> for a company’s domain, or a Stripe Identity session for an individual (test mode today). A verified organization can back a legal_entity anchor.

Billing

There is no plan and no subscription. GET /v0/billing is the overview: the rate card, the month’s usage and the recent invoices. GET /v0/billing/rates is the rate card. GET /v0/billing/usage reads a month’s charges: the fees of the successful payments charged to the organization and, for a platform, its sealed records by band. GET /v0/billing/invoices lists invoices, POST drafts the period’s invoice, and GET /v0/billing/invoices/{period} reads one. SDK billing.*; console /billing.

Policies and rails

Policies are created and signed in the console, or with a developer key through POST /v0/policies (the document; id of an existing policy makes it that policy’s next version). 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). POST /v0/rails/authorize is where a merchant’s payment handler presents the single-use token; POST /v0/ucp/orders is the merchant’s order webhook (order_placed, order_updated with adjustments[], order_shipped).

Sellers

Seller keys use the routes under /v0/sellers/…: orders (observe and list agent orders), verify (check a mandate for a domain), adjustments and reversals (report or request a reversal), fulfillments, records/{id}/dispute-packet and representment.pdf, disputes, reconciliation, conformance, middleware, domains. They are documented with the middleware on the Accept middleware, Adjustments, Dispute packet exports and Conformance pages. GET /badges/{domain}.json and .svg are public.

Added for sellers in this release: GET /v0/sellers/orders/{id}/economics; psp_payment on POST /v0/sellers/orders, answered with psp_write_back; GET and POST /v0/sellers/psp-connections, GET and DELETE …/{id}, POST …/{id}/test, GET and POST …/{id}/disputes, and the PSP webhook POST /v0/psp/{psp}/webhook/{connection_id} (PSP connections); GET and POST /v0/disputes/{id}/deliveries (Dispute delivery); POST and GET /v0/storefronts, GET and PATCH /v0/storefronts/{id}, POST /v0/storefronts/{id}/catalog/refresh, GET /v0/storefronts/{id}/orders (Storefront).

Platforms

Platform keys use /v0/platform/…: accounts (create, list, read, update, issue keys), usage?period=, brand, and feeds (create, list, delete, events, replay); and GET /v0/records/{id}/psp-metadata?psp=. With Enruta-Account, every other route acts for that account. See Platform accounts, Embedded calls, White-label verifier, Dispute field feeds and PSP metadata.

Webhooks

EventWhen
payment_request.decidedA decision was made; its decision carries retryable and retry_after
payment_request.clarification_requestedA decision asked questions; the request waits for the answers
payment_request.handed_offA payment went to a person: the checkout was handed to the merchant’s own page, or the rail issued no credential. The payload carries request_id, agent_id, mandate_id, continue_url, handed_off_at, order, record_id and, from a rail, rail, reason, message and next_step (with mandate_id and order null)
approval.completedAn approval ended: state is approved, declined or expired. One that expired because its consent was revoked with cascades_scheduled carries reason: consent_revoked, and so does one a person tried to approve after the user revoked the consent the decision relied on; one a person tried to approve after the agent was paused carries reason: agent_paused. An approval rolled back because its mandate could not be issued sends nothing
mandate.issuedA mandate exists and can be redeemed
mandate.consumedThe rail authorized against the mandate
record.sealedA record was sealed and time-stamped; carries kind
reversal.completed, reversal.deniedA reversal finished or was refused
settlement.updatedA settlement record changed stage
dispute.outcome_recordedAn outcome was recorded on a dispute packet
reconciliation.report_readyA weekly draft report exists
agent.pausedAn agent was paused
agent.resumedA paused agent was resumed
consent.revokedA consent revocation took effect (not when it was scheduled); the payload carries effective_at, cascades_scheduled, notified_institution_at, cascade, record_id and consent_record_id
recurrence.createdA recurrence was created and its record sealed
recurrence.paused, recurrence.resumedA recurrence was paused or resumed
recurrence.endedA recurrence was ended, also when its schedule reached end_date
recurrence.occurrence_createdAn occurrence became a payment request; the payload carries its state and decision

Deliveries carry an Enruta-Signature header: t=<unix>,v1=<HMAC-SHA256> over the timestamp and body with your endpoint secret. Retries follow an exponential schedule for 24 hours.

Errors

StatusCodeMeaning
400invalid_requestA field is missing or malformed, or looks like a card number
401unauthenticatedNo key, or an unknown key
403forbiddenThe key’s role cannot do this, or a seller key acted outside its verified domains
403account_not_connectedA platform key named an account that is unknown or suspended
404not_foundNo such object in this account
409idempotency_conflictSame key, different body
409invalid_requestThe request is not in a state that allows the call, such as pay-resource without a usable mandate or handoff after a rail hand-off
429rate_limitedSlow down; Retry-After is set

A denied payment or a denied reversal is not an error. It is a 201 with decision.result: "deny", and it produces a record.