API reference
The details a production integration needs: what errors look like, which webhook events fire, how idempotency works, and the rate limits.
Errors
Section titled “Errors”Every non-2xx response is a typed error with a stable machine-readable code and
the matching HTTP status. The SDKs throw it as AgentbankError (err.code,
err.status) — catch that one class to handle failures uniformly.
{ "error": { "code": "PAYMENT_DECLINED", "message": "card declined", "details": { "reason": "insufficient_funds" } } }Branch on code, never on the message text (messages may change; codes are the
contract).
In the SDKs, catch the single AgentbankError class and read err.code / err.status.
Prefer the static AgentbankError.is(err) over err instanceof AgentbankError: at a
package boundary instanceof silently fails when two copies of the SDK’s core coexist in
one node_modules (different class objects), whereas is() checks the shape and
recognizes an error from any version. A transport failure (no HTTP response — timeout,
abort, DNS) surfaces as AgentbankError with status === 0 and code timeout,
aborted, or network_error.
try { await agentbank.checkout.openACP({ currency, items });} catch (err) { if (AgentbankError.is(err) && err.status === 429) { // back off and retry — see Rate limits below }}code |
HTTP | Meaning |
|---|---|---|
VALIDATION_ERROR |
400 | Malformed request — bad/missing field. details carries the field errors. |
UNAUTHORIZED |
401 | Missing or invalid credential. |
PAYMENT_DECLINED |
402 | The rail declined the charge. details.reason has the rail’s reason. |
FORBIDDEN |
403 | Authenticated, but not allowed to touch this resource. |
SANCTIONS_BLOCKED |
403 | Blocked by sanctions screening. |
NOT_FOUND |
404 | No such resource — or one you’re not scoped to see. |
CONFLICT |
409 | State conflict — e.g. capturing an expired checkout, or an idempotency key reused while in flight. |
GONE |
410 | The resource existed but is no longer available. |
RATE_LIMITED |
429 | Too many requests — see Rate limits. |
INVARIANT_VIOLATION |
500 | A server-side consistency check failed. Report it. |
UPSTREAM_ERROR |
502 | A dependency (rail, Curless) failed. May be retryable. |
CIRCUIT_OPEN |
503 | A dependency’s circuit breaker is open (fast-failing after repeated failures). Retry after a short back-off. |
Webhook events
Section titled “Webhook events”Register an endpoint (POST /v1/webhook-endpoints) and subscribe to event types
(subscribe to none = receive all). Each delivery is { id, type, data }, signed
x-agentbank-signature: t=…,v1=…. Verify with the merchant SDK’s verifyWebhook
— see the Webhooks section for verification
and at-least-once dedup.
These are the events a merchant endpoint receives:
| Event | Fires when |
|---|---|
order.paid |
A checkout was charged successfully. |
order.refunded |
An order was refunded (full or partial). |
order.canceled |
An order ended without payment — checkout expired, canceled, or the charge failed. |
payment_intent.captured |
The underlying PaymentIntent captured (fine-grained twin of order.paid). |
payment_intent.settled |
Funds settled (moved from pending to payable). |
payment_intent.refunded |
The PaymentIntent was refunded. |
payment_intent.failed |
A charge was attempted and failed. |
payment_intent.expired |
An unpaid checkout passed its deadline — terminal, never charged. |
payment_intent.canceled |
The PaymentIntent was called off before any charge. |
Order-level events (order.*) are what most integrations want; the
payment_intent.* twins carry the same lifecycle at finer grain for reconciling
against the PaymentIntent directly.
Deliveries are at-least-once — dedupe on event.id, and ack retries with a
2xx (an error re-queues the event). Full guidance in the merchant SDK
Webhooks section.
Idempotency
Section titled “Idempotency”Any mutating request that moves money — opening a checkout, capturing, refunding
— can be retried safely by sending an Idempotency-Key header (Stripe-style).
A retry with the same key replays the original response instead of doing the work
twice.
curl -X POST https://mcp.curless.ai/v1/... \ -H "Authorization: Bearer $KEY" \ -H "Idempotency-Key: order-8f3a-2026-07-19" \ -H "content-type: application/json" \ -d '{ ... }'Behavior:
- Same key, same request → the stored response is replayed (same status and
body), with an
Idempotent-Replayed: trueheader. The work runs once. - Same key, different request body →
400 VALIDATION_ERROR(“Idempotency-Key was already used with different request parameters”). A key is bound to the exact request it first saw. - Same key, original still in flight →
409 CONFLICT(“still being processed; retry shortly”). Retry after a moment. - Handler failed (5xx / crash) → the claim is released, so a retry with the same key is treated as a fresh request (a failure never gets “stuck”).
Keys are scoped per credential and mode, so a test key and a live key
never share an idempotency record (both come from your
Curless wallet — test mode moves no real money,
for end-to-end testing). Choose a key unique to the logical operation (e.g. your own
order id), not per HTTP attempt.
Note: several protocol flows also accept a protocol-level idempotency key (e.g.
the x402 Idempotency-Key), and the gateway dedupes concurrent same-key
PaymentIntent creates internally — but the header above is the general mechanism
for the REST API.
Rate limits
Section titled “Rate limits”A token-bucket limit of 120 requests per 60 seconds (RATE_LIMIT_MAX /
RATE_LIMIT_WINDOW_MS), with burst up to the full 120.
- Authenticated routes —
/v1/*and the protocol paths (/acp,/x402,/a2a,/ap2,/visa-tap,/skyfire,/mpp) — are limited per API key (per principal), so one merchant’s traffic never eats another’s budget. - Public endpoints (OAuth token/authorize/register, the
/mcpresource server, hosted/pay,/v1/integrations) are limited per IP.
Exceeding the limit returns 429 RATE_LIMITED. Back off and retry; combine with
an Idempotency-Key so a retried write can’t double-charge.
Health/readiness probes (/health, /ready) and the Curless inbound webhook
are intentionally not rate-limited (limiting them would false-down load
balancers or drop real refund events).
These are the defaults; talk to us if your integration needs a higher ceiling.

