Error codes
Every error the v1 surface emits uses the envelope from README.md:
{ "success": false, "error": "Human-readable message.", "code": "MACHINE_CODE" }
Branch on code. The error prose may be reworded at any time.
The one exception is 429: no custom handler is registered for rate limiting, so a throttled request answers the limiter's default response rather than this envelope. Treat the status code as the contract there.
The table below lists every code emitted from v2/backend/app/api/v1/.
"Surface" says where it can appear: auth = the API-key decorator
(so any merchant endpoint), invoices = /v1/invoices, addresses
= /v1/addresses, pay = /v1/pay.
| Code | Status | Surface | Meaning |
|---|---|---|---|
ACCOUNT_INACTIVE | 403 | auth | The API key's owning account is not active. |
ACCOUNT_SUSPENDED | 403 | auth | The API key's owning account is suspended. |
ALREADY_REPORTED | 409 | pay | This invoice has already been reported. |
ALREADY_REVIEWED | 409 | pay | A review already exists for this invoice. |
AMBIGUOUS_COIN | 400 | addresses, pay | The coin symbol exists on several networks here; send network. The message lists them. |
COIN_LOCKED | 409 | pay | A different coin is already locked for this invoice and cannot be changed. |
COIN_NOT_ACCEPTED | 400 | addresses, pay | The coin is not enabled on the profile, or is not active. |
COIN_NOT_PAYABLE | 400 | addresses, pay | The coin has no watch-service mapping or confirmation threshold, or the merchant has no active xpub for its chain. |
CONFLICT | 409 | addresses, pay | A concurrency conflict with no readable winner. Nothing was charged - retry. |
EMAIL_EXISTS | 409 | pay | An email was already submitted for this invoice. |
EMAIL_NOT_VERIFIED | 403 | auth | The API key's owning account has not verified its email address. |
EXPIRED | 400 | pay | The invoice payment window has passed. |
GAS_TOO_HIGH | 400 | pay | Current L1 Ethereum gas makes this invoice amount uneconomic. |
IDEMPOTENCY_KEY_REUSED | 409 | addresses | The Idempotency-Key was already used for a different profile/coin combination. |
INSUFFICIENT_CREDITS | 402 | addresses | The merchant has no credits left. Top up to create more addresses. |
INVALID_CURRENCY | 400 | invoices | The requested fiat currency is not supported/active. |
INVALID_EMAIL | 400 | invoices, pay | Not a valid email address. |
INVALID_FIELD | 400 | pay | A note or comment longer than 2000 characters. |
INVALID_IDEMPOTENCY_KEY | 400 | addresses | The Idempotency-Key header exceeds 255 characters. |
INVALID_INVOICE_ID | 400 | invoices | invoice_id is over 255 characters or uses characters outside A-Za-z0-9._-. |
INVALID_PRICE | 400 | invoices | price is not a positive finite number up to 1,000,000. |
INVALID_PROFILE_TYPE | 400 | invoices, addresses | The endpoint is only available for payment profiles. |
INVALID_RATING | 400 | pay | Rating is not an integer between 1 and 5. |
INVALID_REASON | 400 | pay | No report reason exists with that reason_id. |
INVALID_STATUS | 400 | pay | The payment session is completed, cancelled, failed or expired. |
INVALID_URL | 400 | invoices, addresses | A URL field is malformed, or a callback_url is not a valid public URL. |
MERCHANT_UNAVAILABLE | 403 | pay | The merchant cannot accept this payment. Deliberately does not say why: it covers suspended, inactive, deleted and out-of-credits. |
MISSING_FIELDS | 400 | invoices, addresses, pay | A required field is absent or blank. |
NO_COINS | 400 | invoices | The profile has no accepted coins configured. |
NO_RATE | 400 | pay | No usable (non-stale) exchange rate for the selected coin. |
NOT_COMPLETED | 400 | pay | Reviews can only be submitted for completed invoices. |
NOT_FOUND | 404 | invoices, addresses, pay | Unknown or malformed UUID, soft-deleted record, or a record owned by another account. Ownership misses are deliberately indistinguishable from "does not exist". |
NOT_INITIALISED | 400 | pay | The invoice payment session has not been created yet. |
NOT_RECONCILABLE | 409 | invoices | No selected coin, no payable watched address, or the invoice was cancelled. |
PROFILE_INACTIVE | 403 | invoices, addresses | The target profile is not active. |
PROFILE_SUSPENDED | 403 | invoices, addresses | The target profile is suspended. |
RATE_ERROR | 500 | pay | The crypto amount could not be calculated from the rate. |
RATE_UNAVAILABLE | 503 | invoices | The price could not be converted to USD - exchange rates are temporarily unavailable. |
TURNSTILE_FAILED | 400 | pay | Cloudflare Turnstile human verification failed. |
UNAUTHORIZED | 401 | auth | Missing, unknown or revoked API key. |
WATCH_REGISTRATION_FAILED | 502 | addresses, pay | Payment monitoring could not be registered. Everything is rolled back, including the claimed credit. |
WATCH_UNAVAILABLE | 502 | invoices | The watch service could not be reached, or the invoice has no active watch. |
Notes on specific behaviours
- ·404 over 403 for ownership. Reading another merchant's invoice or
address request returns
NOT_FOUND, not a permission error. A malformed UUID returns the same thing, so probing tells an attacker nothing. - ·Payer endpoints never leak merchant state.
MERCHANT_UNAVAILABLEis the single public answer for suspended, inactive, deleted and out-of-credits merchants; the real reason is logged server-side and sent to the merchant's own notification channels. - ·Rollback on failure is complete. When address or coin selection
fails after a credit was claimed (
WATCH_REGISTRATION_FAILED, a lost idempotency race), the transaction is rolled back: the credit is refunded and no watch is left pointing at a row that does not exist.