Skip to content

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-After and no RateLimit-* 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.

maxanapay.js exposes these source-defined codes:

  • config_error
  • origin_not_allowed
  • session_expired
  • session_completed
  • provider_unsupported
  • rate_limited
  • payment_declined
  • capture_pending
  • paypal_sdk_error
  • network_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.

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);
  },
});

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:

OperationBudget
DELETE /api/channel/webhook-endpoints/{endpoint_id}20 per 1 minute
GET /api/channel/api-keys60 per 1 minute
GET /api/channel/merchants60 per 1 minute
GET /api/channel/onboarding/status60 per 1 minute
GET /api/channel/statements60 per 1 minute
GET /api/channel/statements/{statement_id}60 per 1 minute
GET /api/channel/statements/{statement_id}/lines60 per 1 minute
GET /api/channel/webhook-endpoints60 per 1 minute
GET /api/checkout-sessions/{checkout_session_id}300 per 1 minute
GET /api/merchants/disputesNo per-operation budget
GET /api/merchants/disputes/{dispute_id}No per-operation budget
GET /api/text-to-pay/requestsNo per-operation budget
GET /api/text-to-pay/requests/{request_id}No per-operation budget
GET /api/transactions120 per 1 minute
GET /api/transactions/daily-revenue120 per 1 minute
GET /api/transactions/filter-options120 per 1 minute
GET /api/transactions/stats120 per 1 minute
GET /api/transactions/{transaction_id}120 per 1 minute
GET /api/transactions/{transaction_id}/provider-detail120 per 1 minute
GET /api/vault/tokens120 per 1 minute
PATCH /api/channel/webhook-endpoints/{endpoint_id}20 per 1 minute
POST /api/channel/api-keys10 per 1 minute
POST /api/channel/api-keys/{key_id}/revoke20 per 1 minute
POST /api/channel/merchants20 per 1 minute
POST /api/channel/onboarding/start20 per 1 minute
POST /api/channel/webhook-endpoints20 per 1 minute
POST /api/channel/webhook-endpoints/{endpoint_id}/enable20 per 1 minute
POST /api/channel/webhook-endpoints/{endpoint_id}/rotate-secret10 per 1 minute
POST /api/checkout-sessions60 per 1 minute
POST /api/checkout-sessions/{checkout_session_id}/client-token60 per 1 minute
POST /api/merchants/disputes/{dispute_id}/accept-claim10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/acknowledge-return-item10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/appeal10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/escalate10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/make-offer10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/provide-evidence10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/provide-supporting-info10 per 1 minute
POST /api/merchants/disputes/{dispute_id}/send-message10 per 1 minute
POST /api/payments/captures/{capture_id}/refund10 per 1 minute
POST /api/payments/gravy/embed-token60 per 1 minute
POST /api/payments/nmi/tokenization-key60 per 1 minute
POST /api/payments/orders60 per 1 minute
POST /api/payments/orders/{order_id}/capture60 per 1 minute
POST /api/text-to-pay/requests20 per 1 minute
POST /api/text-to-pay/requests/{request_id}/cancel30 per 1 minute
POST /api/vault/charge10 per 1 minute

An operation with no per-operation budget is not unlimited; it is only not limited by a budget of its own.

Exceeding a budget answers 429. Two things about that response cost people time:

  • Its body key is error, not the detail every validation and authentication error uses. A client that reads only detail logs an empty reason.
  • It carries no header telling you when to retry — no Retry-After, and none of the RateLimit- 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.

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.