The gateway returns standard HTTP status codes. Error bodies are plain text strings (not JSON envelopes) — designed to be human-readable in curl output.
| Body | Meaning | Fix |
|---|
missing required fields: … | A required body field is empty. | Send the listed fields. See per-endpoint reference for which are required. |
invoice_prefix must match … | Prefix doesn’t match ^[A-Z][A-Z0-9-]{0,11}$. | Uppercase letters / digits / dashes only, 1–12 chars, must start with a letter. |
body read (on inbound webhooks) | Body couldn’t be read. | Network glitch; retry. |
| Body | Meaning | Fix |
|---|
missing auth headers | One of X-PAY-Key / X-PAY-Timestamp / X-PAY-Signature absent. | Check spelling — exact case X-PAY-Key. |
bad timestamp | Timestamp not parseable as integer. | Send Unix epoch seconds, integer. |
timestamp out of range | |now − ts| > 300s. | NTP your machine. |
bad signature | HMAC mismatch. | See Authentication common-mistakes table. |
bad signature (on /webhooks/<psp> endpoints) | Inbound PSP signature invalid. | Operator verifies env secret matches PSP dashboard. Not your problem unless you’re operating the gateway. |
| Body | Meaning |
|---|
payment not found | The id doesn’t exist OR doesn’t belong to your app. (Tenant scoping is enforced — you can’t read another product’s payments.) |
unknown payment (on inbound webhooks) | The PSP referenced a payment_id we don’t have. Usually means the PSP is replaying an event from before your app existed. Operator investigates. |
| Body | Meaning | Fix |
|---|
idempotency conflict — different body for same key | You reused Idempotency-Key with different request body within 24h. | Use a fresh key OR send the exact same body. |
| Body | Meaning |
|---|
no provider supports method=X for currency=Y | Routing found no PSP for the (currency, method) combo. Check Routing. |
provider X not allowed for this app | App’s allowed_providers blocks the picked PSP. Operator extends the allowlist. |
method X not enabled for provider Y on this app | App’s allowed_methods[Y] doesn’t include this method. |
payment must be in 'succeeded' state to refund (got X) | You tried to refund a non-succeeded payment. |
payment already has a refund | Only one refund per payment today. |
| Body | Meaning |
|---|
payment create failed | DB or other gateway-internal failure. Retryable — try again with the same Idempotency-Key. |
customer upsert failed | Same — DB hiccup. Retryable. |
store provider_ref | The PSP returned a checkout but persisting it failed. Rare; operator investigates. |
| Body | Meaning |
|---|
provider checkout failed: … | PSP rejected the registration. The PSP’s verbatim error is included — common cases per-PSP guides. |
refund failed: … | PSP rejected the refund (window expired, already refunded, etc.). |
The gateway is down or rolling out. Retryable.
These show up in the PSP’s webhook delivery dashboard, not yours. Listed for ops:
| Status | When |
|---|
200 acknowledged:true,action:none | Event received, signature valid, but it’s not a status-changing type (PSP heartbeat, intermediate state). No-op. |
200 acknowledged:true,action:already_finalized | Event valid, but the payment was already moved to a terminal state by an earlier event. Idempotent no-op. |
200 acknowledged:true | Normal success path; payment was finalized; outbound delivery enqueued. |
400 bad reference … | PSP-side reference (their order id) couldn’t be resolved to a gateway payment. |
401 bad signature | PSP signature didn’t verify. Operator checks env secret against PSP dashboard. |
If a payment is misbehaving, the operator can:
GET /admin/payments/{id} — full row including last_error.
GET /admin/inbound_events?payment_id=… — every PSP webhook archive entry for this payment, including signature_valid + raw body.
These are not exposed to product apps. Ask ops if you need a deep look.