Skip to main content

Troubleshooting: payment-mandate confirmation failures

An AP2 payment mandate is the signed, spend-capped consent an AI agent transacts under: a caller pre-authorizes a per-transaction maximum, a total cap, a currency, an allow-list of merchants and categories, and an expiry, and the agent can then charge carts against that mandate without asking per-cart. On a live voice call the agent reads the cart (amount, currency, merchant) back to the caller, gets explicit verbal approval, and invokes the confirmation tool — the request this page triages. For the mandate concept itself, read Commerce checkout spine and the Conversational Commerce Checkout guide first. The confirmation has three customer-facing failure codes, and all three fail closed — the customer is never charged on ambiguity. A failed response means nothing persisted, so the whole mandate decision must be resolved before you retry. Read the error envelope first: the error.code field tells you which gate rejected the request. A typical envelope:

Cause table

Charge commit — MANDATE_CHARGE_FAILED (500)

This 500 raises when the mandate decision approved the cart but the commit step threw — the one failure where authorization succeeded and persistence failed. Before you treat it as a platform fault, check the mandate’s scope constraints: the most common trigger is a cart the mandate should never have approved — an amount past the per-transaction maximum, a merchant or category outside the allow-list, or a mandate that lapsed to expired, revoked, or exhausted between session-create and the confirmation call. Check first:
  1. Amount within the per-transaction max. Read the mandate from the voice session-create payload (payment_mandate) and compare charge.amount against maxPerTransaction. A cart above the max must be split or the mandate re-issued with a higher max — no retry of the same cart will pass.
  2. Merchant and category on the allow-lists. charge.merchant must appear in allowedMerchants (and the agent’s category in allowedCategories). Correct the catalog mapping at the agent layer and re-read the cart to the caller before re-confirming.
  3. Mandate status and expiry. A mandate moves to expired, revoked, or exhausted over the course of a long call; the agent should re-check mandate.status before invoking the confirmation tool.
If the scope checks out clean and the same 500 persists with a consistent request_id, escalate (below) — that is a genuine commit failure, not a scope mismatch.

Cap verification — MANDATE_SPEND_UNVERIFIABLE (503)

The mandate’s total cap is cumulative: every confirmed cart under the mandate spends against totalCap, and a new cart may only be authorized if spent + charge.amount ≤ totalCap. To authorize safely, the confirmation must read the prior confirmed spend — and it refuses rather than authorize against a position it could not read or record. That refusal is this 503. Treat it as a temporary-unavailable response, semantics identical to the read-path health checks elsewhere in the platform. Do not re-authorize at the agent layer — do not fall back to “assume it fits under the cap” and never retry in a tight loop. Retry with backoff once the dependency is healthy again; the customer was not charged on any ambiguity. If the 503 persists past a few minutes, escalate with the request_id — a standing 503 here means the confirmation cannot establish the mandate’s cumulative position at all.

Reference discipline — IDEMPOTENCY_CONFLICT (409)

A single mandate legitimately funds many distinct carts in one call, so the replay token is the per-charge charge.reference — the cart or order id the agent read back to the caller — keyed together with the call and the mandate. A retry of the same cart (a network blip, a gateway restart) returns the stored confirmation without charging twice. A different cart (amount, currency, or merchant changed) under the same reference receives this 409 and is never replayed as confirmed. The 409 is a stop-and-reconcile signal, not a retry signal:
  1. Pull the confirmed charges for the call (see the audit-trail section) and compare the recorded cart against the cart you are trying to confirm.
  2. In your order system, resolve which cart the reference actually belongs to. If the original cart was the right one, discard the new request. If the cart genuinely changed (an agent-side reprice mid-call), issue a new order id and re-read the full cart to the caller before confirming with the fresh reference.
Assign references only at cart-finalize time, and never reuse one across agents, sessions, or reprice attempts — the conflict gate exists to catch exactly that reuse.

Audit trail

Every successful confirmation is stamped onto the call record: read GET /api/v1/voice/calls/:id and look at metadata.payment_mandate_confirmation. The stamp carries the mandate id, the authorization id, the consent digest, the amount/currency/merchant, the reference, whether the AP2 verifiable credential was issued, the recording egress id, and the confirmation timestamp — the same call row the recording lives on, so a reviewer pulling the recording sees the cryptographic authorization alongside it. Use it to reconcile 409 conflicts and to prove what was (and was not) charged.

Retry-safety and escalation

All three codes converge on one rule: a failure means the customer was not charged, and none resolve by retrying the identical request unchanged:
  • MANDATE_CHARGE_FAILED — fix the mandate-scope mismatch first, then retry the identical cart (safe: a failed commit persisted nothing).
  • MANDATE_SPEND_UNVERIFIABLE — retry only after the dependency recovers, with backoff. Never re-authorize at the agent layer.
  • IDEMPOTENCY_CONFLICT — reconcile the reference before confirming again; a new cart needs a new reference.
Escalate to Devotel support when the cause table points at a genuine platform fault (a persisting 500 with clean scope, or a standing 503). Attach the request_id from the error envelope, the call id, the mandate id, and the charge reference — the support team reads the same payment_mandate_confirmation audit stamp on the call metadata that this page polls.

Cross-references