Errors, declines, and rate limits
Classify failures before retrying:
- Validation and authentication failures require a request or credential fix.
- A payment decline is a payment outcome. Let the buyer choose another method; do not loop the same attempt automatically.
- A rate limit requires backoff. Maxana sends no
Retry-Afterand noRateLimit-*header, so back off on a schedule of your own: wait, apply jitter, and cap retries. The budgets are published under Rate limits. - A timeout or network interruption can leave an uncertain outcome. Read the resource or retry with the same idempotency key instead of creating a second payment intent.
- Server failures may be transient, but retry only operations whose contract and idempotency behavior make that safe.
Public request bodies are strict. A checkout-session request with an
unrecognized top-level property or an unrecognized property within an items
entry returns 422 Unprocessable Entity; the validation error names the
unknown field instead of silently dropping it.
Read onboarding blockers from status, not from a payment error
Section titled “Read onboarding blockers from status, not from a payment error”You do not learn ahead of time that a channel merchant cannot yet take payments
from an HTTP error or an SDK code. Attempt checkout for such a merchant and it
is refused with 403, reaching the SDK as
origin_not_allowed — a signal that
something is wrong, not what. What is wrong is a field —
paypal_status_reason — in the onboarding status
read, where every
value it can carry is listed with what it means and what to do about it.
One value reads like a dead end and is not. paypal_account_already_linked
means the PayPal account the merchant chose is already attached to a different
Maxana merchant; {"reconnect": true} does not resolve the conflict by itself.
Have the merchant onboard a different PayPal account, or ask Maxana to release
the existing link.
SDK error vocabulary
Section titled “SDK error vocabulary”maxanapay.js exposes these source-defined codes:
config_errororigin_not_allowedsession_expiredsession_completedprovider_unsupportedrate_limitedpayment_declinedcapture_pendingpaypal_sdk_errornetwork_error
Route each code to an explicit experience: configuration faults to developers, origin faults to deployment configuration, terminal session outcomes to a new server-created session, declines to buyer recovery, throttling to backoff, and network uncertainty to reconciliation.
Two codes are wider than their names
Section titled “Two codes are wider than their names”Both of these send a reader hunting through their own page when the fault is
somewhere else, so read the status and message the error carries rather than
branching on the code alone.
origin_not_allowed is every 403 the API answers, not only a page served from
an origin the session did not authorize. A merchant that is not allowed to take
a payment is refused with 403 when the payment is created, and arrives in
onError under this code as well. Check the merchant’s readiness before you
check the origin list.
config_error is the fallback for any refusal the SDK has no more specific code
for — a 400, 404 or 422 it cannot classify. A payment the provider refuses
because of the merchant’s own account reaches it, and so does an unrecognized
request field. Neither is a mount misconfiguration, and neither is fixed in the
browser.
Route stable SDK error codes to your own recovery paths
MaxanaPay.checkout({
sessionId: 'cs_9f2c...',
container: '#paypal-buttons',
environment: 'sandbox',
onError(error) {
if (error.code === 'config_error') {
console.error('Fix the checkout configuration before retrying.');
return;
}
console.error(error.code, error.retryable);
},
});Rate limits
Section titled “Rate limits”Most published operations carry a budget of their own. A credentialed call is counted per credential — two channels never spend each other’s, and neither do two operations. The buyer-facing calls that carry no secret key, reading a checkout session among them, are counted per source address instead, so callers sharing an address share that budget. A few operations set no per-operation budget at all, shown below as such. The budgets in force are:
| Operation | Budget |
|---|---|
DELETE /api/channel/webhook-endpoints/{endpoint_id} | 20 per 1 minute |
GET /api/channel/api-keys | 60 per 1 minute |
GET /api/channel/merchants | 60 per 1 minute |
GET /api/channel/onboarding/status | 60 per 1 minute |
GET /api/channel/statements | 60 per 1 minute |
GET /api/channel/statements/{statement_id} | 60 per 1 minute |
GET /api/channel/statements/{statement_id}/lines | 60 per 1 minute |
GET /api/channel/webhook-endpoints | 60 per 1 minute |
GET /api/checkout-sessions/{checkout_session_id} | 300 per 1 minute |
GET /api/merchants/disputes | No per-operation budget |
GET /api/merchants/disputes/{dispute_id} | No per-operation budget |
GET /api/text-to-pay/requests | No per-operation budget |
GET /api/text-to-pay/requests/{request_id} | No per-operation budget |
GET /api/transactions | 120 per 1 minute |
GET /api/transactions/daily-revenue | 120 per 1 minute |
GET /api/transactions/filter-options | 120 per 1 minute |
GET /api/transactions/stats | 120 per 1 minute |
GET /api/transactions/{transaction_id} | 120 per 1 minute |
GET /api/transactions/{transaction_id}/provider-detail | 120 per 1 minute |
GET /api/vault/tokens | 120 per 1 minute |
PATCH /api/channel/webhook-endpoints/{endpoint_id} | 20 per 1 minute |
POST /api/channel/api-keys | 10 per 1 minute |
POST /api/channel/api-keys/{key_id}/revoke | 20 per 1 minute |
POST /api/channel/merchants | 20 per 1 minute |
POST /api/channel/onboarding/start | 20 per 1 minute |
POST /api/channel/webhook-endpoints | 20 per 1 minute |
POST /api/channel/webhook-endpoints/{endpoint_id}/enable | 20 per 1 minute |
POST /api/channel/webhook-endpoints/{endpoint_id}/rotate-secret | 10 per 1 minute |
POST /api/checkout-sessions | 60 per 1 minute |
POST /api/checkout-sessions/{checkout_session_id}/client-token | 60 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/accept-claim | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/acknowledge-return-item | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/appeal | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/escalate | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/make-offer | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/provide-evidence | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/provide-supporting-info | 10 per 1 minute |
POST /api/merchants/disputes/{dispute_id}/send-message | 10 per 1 minute |
POST /api/payments/captures/{capture_id}/refund | 10 per 1 minute |
POST /api/payments/gravy/embed-token | 60 per 1 minute |
POST /api/payments/nmi/tokenization-key | 60 per 1 minute |
POST /api/payments/orders | 60 per 1 minute |
POST /api/payments/orders/{order_id}/capture | 60 per 1 minute |
POST /api/text-to-pay/requests | 20 per 1 minute |
POST /api/text-to-pay/requests/{request_id}/cancel | 30 per 1 minute |
POST /api/vault/charge | 10 per 1 minute |
An operation with no per-operation budget is not unlimited; it is only not limited by a budget of its own.
What a throttled response looks like
Section titled “What a throttled response looks like”Exceeding a budget answers 429. Two things about
that response cost people time:
- Its body key is
error, not thedetailevery validation and authentication error uses. A client that reads onlydetaillogs an empty reason. - It carries no header telling you when to retry — no
Retry-After, and none of theRateLimit-family. There is nothing to honour, so schedule the wait yourself.
The message inside the body repeats the budget that was exceeded, so the response itself tells you which of the budgets above you hit without instrumenting the call site.
Current limits
Section titled “Current limits”An error response or browser callback cannot establish a payment’s durable outcome after a timeout or network interruption. Reconcile the resource and use the same idempotency key where supported. The SDK error vocabulary applies to the published browser bundle, but provider and eligibility failures still require a buyer-safe fallback.