Skip to main content
All Proof Proxy API errors return a JSON body:
{
  "error": "error_code",
  "message": "Human-readable description",
  "request_id": "req_abc123"
}
Use error for programmatic handling and message for logging.

HTTP Error Codes

HTTP StatusError CodeDescriptionAction
400invalid_requestMissing or invalid request parametersCheck required fields in request
400unsupported_currencyCurrency or network not enabled for your integrationContact Proof team to enable
401unauthorizedMissing or invalid client_tokenCheck your token; rotate if compromised
403forbidden_originRequest origin not whitelistedCheck your registered domain; contact Proof team
403offramp_disabledOff-ramp not enabled for your integrationRequest off-ramp access from Proof team
404transaction_not_foundTransaction ID does not existVerify the merchant_transaction_id
409session_already_usedinit_token has already been usedRequest a new session
422kyc_requiredUser must complete KYC before transactingEmbed widget — it will handle KYC automatically
429rate_limit_exceededToo many requestsSlow down; implement exponential backoff
500internal_errorProof internal errorRetry once; contact support if persistent

Common Scenarios

kyc_required (422)

{
  "error": "kyc_required",
  "message": "User must complete KYC verification before transacting",
  "partner_user_id": "user-123"
}
Recommended handling: embed the widget anyway — it will automatically start the KYC flow for the user. Or, check KYC status first and display a custom message:
const kyc = await fetch(
  `https://DOMAIN/widget/users/${userId}/kyc-status`,
  { headers: { "Authorization": "Bearer <client_token>" } }
).then(r => r.json());

if (kyc.status !== "verified") {
  showMessage("Please verify your identity before buying crypto");
} else {
  openWidget();
}

session_already_used (409)

The init_token is single-use and expires after first use or 1 hour. If you see this error, request a new session.
// Always request a fresh session before opening the widget
const session = await requestNewSession(userId, userEmail);
launchWidget(session);

rate_limit_exceeded (429)

Implement exponential backoff for polling:
async function pollWithBackoff(txId, attempt = 0) {
  const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
  await new Promise(resolve => setTimeout(resolve, delay));

  const tx = await fetch(
    `https://DOMAIN/widget/transactions/${txId}`,
    { headers: { "Authorization": "Bearer <client_token>" } }
  ).then(r => r.json());

  if (["completed", "failed", "cancelled"].includes(tx.status)) {
    return tx;
  }

  return pollWithBackoff(txId, attempt + 1);
}

forbidden_origin (403)

If you see this error, the Origin header of the request does not match your registered domain or bundle ID.
  • Verify the domain you provided during onboarding matches the actual request origin.
  • If testing from a new domain or localhost, contact the Proof team to update your whitelist.

Widget-Level Errors

The onStatusChange callback also fires on payment failures:
onStatusChange: function(data) {
  if (data.status === "order_failed") {
    // Payment failed — user can retry inside widget
    console.log("Payment failed:", data.merchantTransactionId);
  }
}
The user can retry inside the widget without requesting a new session. See Transaction Status for the full status list.