Error Codes
How errors are delivered
| Channel | When | What you receive |
|---|---|---|
onError(code, message) | A deposit, withdrawal, or session operation fails | A machine-readable code and a human-readable message safe to display |
PROPHETX_SESSION_EXPIRED | The JWT's exp claim has passed | A standalone event — respond with a fresh token |
PROPHETX_GEOFENCE_BLOCKED | User's location is restricted | stateCode and reason |
PROPHETX_KYC_FAILED | Identity verification was rejected | reason string |
The code values are stable strings. The message values are human-readable and may change — never match on them programmatically.
Error response shape
Every onError callback receives the same two arguments:
onError: (code: string, message: string) => voidIf you're working with the low-level postMessage protocol instead of the SDK, error payloads arrive as:
{
"type": "PROPHETX_ERROR",
"payload": {
"code": "DEPOSIT_FAILED",
"message": "The deposit could not be completed."
}
}Session errors
These errors fire when the embed cannot establish or maintain a valid session. They typically indicate a problem with your token endpoint or key registration — not something the end user can fix.
| Code | HTTP | Retryable | Cause |
|---|---|---|---|
SESSION_EXPIRED | — | Yes | The JWT's exp claim is in the past |
SESSION_INVALID | 401 | No | Malformed JWT, bad signature, wrong issuer, or server-side rejection |
ORIGIN_UNAUTHORIZED | 403 | No | Your page's origin is not in your partner-registered allowlist |
SESSION_EXPIRED
SESSION_EXPIREDThe most common session error. Tokens are short-lived (5 minutes max), so this will happen if a user takes time in a flow.
What to do: Return a fresh token from your onSessionExpired handler. The SDK handles the refresh automatically if you provided this callback. If you're using the low-level postMessage protocol, listen for PROPHETX_SESSION_EXPIRED and respond with PROPHETX_SESSION_REFRESH.
SESSION_INVALID
SESSION_INVALIDThe token failed validation — either client-side (malformed JWT, missing claims) or server-side (bad signature, unrecognized issuer). The server returns a bare 401 with no response body, so the specific cause isn't returned over the wire — investigate from your side.
What to do: Do not retry with the same token. Check your signing implementation:
- Verify you're using the
EdDSAalgorithm with an Ed25519 private key - Confirm the
issclaim matches your ISV ID - Confirm the
subclaim is a valid ProphetX user ID - Ensure the token hasn't been tampered with after signing
ORIGIN_UNAUTHORIZED
ORIGIN_UNAUTHORIZEDThe embed detected that your page's origin isn't in the partner-registered allowlist.
What to do: Register your origin with ProphetX. This is a one-time setup per domain. If you're seeing this in development, make sure your localhost origin (including port) is registered. See Deposits and Withdrawals — Before you begin.
User prerequisite errors
Alpha — Not yet activeThese error codes are documented for forward compatibility but are not currently triggered. They will become active in an upcoming alpha release — we recommend implementing handlers for them now so your integration is ready.
When the embed initializes, it calls POST /api/v1/session/validate and receives a gates object describing which user prerequisites are satisfied:
{
"expiration": "2026-06-15T16:30:00Z",
"isvId": "80c7cadf-22e4-4752-b821-ef3e2c8f6cb4",
"userId": "4874c17c-d991-496e-b29c-c7bdba87698d",
"gates": {
"email-verify": { "completed": true, "description": "Email address verified" },
"geoip": { "completed": true, "description": "IP address in US" },
"kyc": { "completed": true, "description": "Passed KYC" },
"phone-verify": { "completed": true, "description": "Phone number verified" },
"terms": { "completed": true, "description": "Accepted terms and conditions" }
},
"permissions": { /* see "Permission errors" below */ }
}When any gate has completed: false, the SDK fires onError(code, message) with the corresponding code below. message carries the gate's description so you can display it directly.
| Code | Source gate | Retryable | Cause |
|---|---|---|---|
TERMS_NOT_SIGNED | gates.terms | No | User has not accepted ProphetX's terms and conditions |
EMAIL_NOT_VERIFIED | gates.email-verify | No | User's email address has not been verified |
PHONE_NOT_VERIFIED | gates.phone-verify | No | User's phone number has not been verified |
GEOFENCE_BLOCKED | gates.geoip | No | User's IP is outside a permitted jurisdiction — see Geofence errors |
Why isn't KYC in this table? ProphetX user accounts cannot be created without passing KYC, so
gates.kyc.completedis alwaystruefor any user who has a session token. You will not see a KYC-related prerequisite error from the validate gates. (Note:KYC_REQUIREDmay still fire on a withdraw attempt — that is a separate withdrawal-specific gate; see Withdrawal errors.)
EMAIL_NOT_VERIFIED
EMAIL_NOT_VERIFIEDThe user's email address has not been verified. There is no embedded SDK flow for this today — verification is handled by your registration system or by an out-of-band email link.
What to do: Prompt the user to verify their email (e.g., re-send a verification email from your backend, or open a "check your inbox" UI). Retry the deposit or withdrawal once gates.email-verify.completed === true on the next session.
PHONE_NOT_VERIFIED
PHONE_NOT_VERIFIEDThe user's phone number has not been verified.
What to do: Prompt the user to verify their phone (OTP flow on your side, or trigger the onboarding flow which includes phone verification as part of identity verification). Retry once verified.
TERMS_NOT_SIGNED
TERMS_NOT_SIGNEDThe user has not accepted ProphetX's terms and conditions, which is required before any transaction.
What to do: Open the terms acceptance flow with the standalone hook, then retry the original deposit or withdrawal once onTermsAccepted fires.
If your registration flow doesn't pre-collect terms acceptance, open
useOnboardingto walk the user through terms and an optional first deposit in one guided flow.
Onboarding outcome events
The codes above tell you why a transaction was blocked. Once you open the onboarding embed, verification outcomes (
PROPHETX_KYC_SUCCESS,PROPHETX_KYC_PENDING,PROPHETX_KYC_FAILED) are delivered as dedicated postMessage events — see postMessage Protocol for details.
Permission errors
Alpha — Contract finalizingThe validate response includes a
permissionsobject describing what the user is allowed to do. The partner-facing error surface for permission denials is still being finalized — treat the codes below as a forward-looking sketch.
In addition to the gates object, POST /api/v1/session/validate returns a permissions map keyed by action. Each entry has granted: boolean, a human-readable description, and an optional denyReason when access is refused:
{
"permissions": {
"deposit-trustly": { "granted": true, "description": "Deposit from Trustly" },
"deposit-zerohash": { "granted": false, "denyReason": "Blocked in NY", "description": "Deposit from Zerohash" },
"withdraw-trustly": { "granted": true, "description": "Withdraw to Trustly" },
"withdraw-zerohash": { "granted": false, "denyReason": "Blocked in NY", "description": "Withdraw to Zerohash" },
"market-order": { "granted": true, "description": "Submit market orders" },
"parlay": { "granted": true, "description": "Submit parlay orders" }
}
}When a user attempts an action they aren't granted, the SDK fires onError with ACTION_NOT_PERMITTED. The denyReason from the validate response is passed through in message.
| Code | Retryable | Cause |
|---|---|---|
ACTION_NOT_PERMITTED | No | The action the user attempted is not granted for this user. message carries the server's denyReason. |
What to do: Display the denyReason so the user understands why the action is blocked (e.g., "Blocked in NY"). For provider-specific permissions like deposit-zerohash, the embed transparently falls back to a permitted provider when one is available — you generally don't need to intervene unless every provider for that action is denied.
Deposit errors
These errors fire through the onError callback when a deposit fails at any stage — initiation, provider interaction, or finalization.
| Code | Retryable | Cause |
|---|---|---|
DEPOSIT_FAILED | Yes | The deposit API returned an error during initiation or submission |
DEPOSIT_NOT_COMPLETED | No | Deposit was initiated but finalization returned a non-completed status |
TRUSTLY_LOAD_FAILED | Yes | The Trustly payment widget failed to load |
TRUSTLY_PROVIDER_ERROR | Yes | Trustly returned an error during the payment flow |
ZEROHASH_PROVIDER_ERROR | Yes | ZeroHash returned an error during the payment flow |
DEPOSIT_FAILED
DEPOSIT_FAILEDThe most common deposit error. The backend rejected the deposit request — the message contains the reason (e.g., amount exceeds limit, invalid payment method, wallet error).
What to do: Display the message to the user. The embed already shows an error UI with a retry option, so you don't need to close the modal. Log the code and message for debugging.
DEPOSIT_NOT_COMPLETED
DEPOSIT_NOT_COMPLETEDThe deposit was initiated and submitted to the payment provider, but when the embed checked the final status, the transaction was not marked as completed. This can happen if the provider approved the payment but the backend hasn't reconciled it yet.
What to do: Don't treat this as a hard failure. The deposit may still complete asynchronously. Check the transaction status on your backend using the transactionId from earlier in the flow, or poll the wallet balance. Do not prompt the user to retry immediately — they may end up with a duplicate deposit.
TRUSTLY_LOAD_FAILED
TRUSTLY_LOAD_FAILEDThe Trustly Lightbox widget (an external third-party SDK) failed to load. This is almost always a network or CDN issue on the user's end.
What to do: The user can retry. If the problem persists, suggest they check their internet connection or try a different payment method.
TRUSTLY_PROVIDER_ERROR
TRUSTLY_PROVIDER_ERRORTrustly's widget loaded successfully, but returned an error during the payment flow — for example, the user's bank rejected the transaction, or Trustly experienced an internal error.
What to do: Display the message to the user. They can retry with the same or a different payment method.
ZEROHASH_PROVIDER_ERROR
ZEROHASH_PROVIDER_ERRORZeroHash returned an error during the payment or onboarding flow.
What to do: Display the message. The user can retry.
Withdrawal errors
These errors fire through the onError callback when a withdrawal fails.
| Code | Retryable | Cause |
|---|---|---|
WITHDRAWAL_FAILED | Yes | The withdrawal API returned an error |
KYC_REQUIRED | No | User has not completed identity verification |
WITHDRAWAL_FAILED
WITHDRAWAL_FAILEDThe backend rejected the withdrawal request. Common causes: insufficient balance, invalid payment method, or a provider-side error.
What to do: Display the message to the user. The embed shows an error UI with retry. Log for debugging.
KYC_REQUIRED
KYC_REQUIREDThe user attempted a withdrawal but hasn't completed identity verification (KYC). The backend returns HTTP 403 with a KYC_REQUIRED code.
What to do: Open the onboarding flow so the user can complete verification, then allow them to retry the withdrawal. Once verified, all future withdrawals succeed without interruption.
When the user prerequisite errors become active,
KYC_NOT_STARTEDwill cover this case for both deposits and withdrawals. We recommend handling both codes now.
Geofence errors
| Code | Retryable | Cause |
|---|---|---|
GEOFENCE_BLOCKED | No | User is in a US state where transactions are not permitted |
GEOFENCE_BLOCKED
GEOFENCE_BLOCKEDThe user's geolocation was checked before a deposit or withdrawal, and they're in a restricted jurisdiction.
This error fires through two channels simultaneously:
onError("GEOFENCE_BLOCKED", message)— the standard error callbackPROPHETX_GEOFENCE_BLOCKEDpostMessage — a dedicated event withstateCodeandreason
If you're using the SDK hooks, you can also listen via the dedicated onGeofenceBlocked callback on useDeposit / useWithdraw:
What to do: Display a location-restricted message. The user cannot proceed from this state — do not offer a retry. The embed shows its own blocked UI, but you may want to update your surrounding page to reflect the restriction.
Runtime errors
| Code | Retryable | Cause |
|---|---|---|
UNCAUGHT_ERROR | Yes | An unexpected runtime error inside the embed |
UNCAUGHT_ERROR
UNCAUGHT_ERRORA catch-all for unhandled exceptions in the embed's React error boundary. This should be rare — it indicates a bug in the embed itself, not a user or integration problem.
What to do: Log the message for debugging and ask the user to retry. If the error persists, contact ProphetX support with the error details.
Network-level errors
These errors come from the SDK's HTTP client layer when requests to the ProphetX API fail at the network level. They appear in the onError callback but originate before any business logic runs.
| Code | Retryable | Cause |
|---|---|---|
TIMEOUT | Yes | The API request exceeded the timeout threshold (15 seconds) |
NETWORK_ERROR | Yes | The browser couldn't reach the API (DNS failure, offline, CORS block) |
TIMEOUT
TIMEOUTA request to the ProphetX API didn't receive a response within 15 seconds.
What to do: The user can retry. If timeouts are frequent, check the user's network connection. Server-side, transient timeouts on status codes 408, 500, 502, 503, and 504 are retried automatically by the SDK's HTTP client.
NETWORK_ERROR
NETWORK_ERRORThe browser failed to establish a connection. Common causes: the user is offline, a corporate proxy is blocking the request, or there's a DNS resolution failure.
What to do: Suggest the user check their internet connection and retry. If you're seeing this consistently in development, verify that your CSP (Content Security Policy) headers allow connections to the ProphetX API domain.
Complete error handling example
A single handler that covers all error codes across deposit and withdrawal flows:
Quick reference
Every error code on one page, sorted by category.
| Code | Category | Retryable | Summary |
|---|---|---|---|
SESSION_EXPIRED | Session | Yes | JWT expired — refresh the token |
SESSION_INVALID | Session | No | Bad JWT — check signing implementation |
ORIGIN_UNAUTHORIZED | Session | No | Origin not registered — contact ProphetX |
DEPOSIT_FAILED | Deposit | Yes | Deposit API error — user can retry |
DEPOSIT_NOT_COMPLETED | Deposit | No | Deposit pending — check status, don't retry |
TRUSTLY_LOAD_FAILED | Deposit | Yes | Trustly widget didn't load — network issue |
TRUSTLY_PROVIDER_ERROR | Deposit | Yes | Trustly error during payment — user can retry |
ZEROHASH_PROVIDER_ERROR | Deposit | Yes | ZeroHash error — user can retry |
WITHDRAWAL_FAILED | Withdrawal | Yes | Withdrawal API error — user can retry |
KYC_REQUIRED | Withdrawal | No | Withdrawal-specific KYC gate — open onboarding |
TERMS_NOT_SIGNED | Prerequisite | No | T&C not accepted — open useTerms |
EMAIL_NOT_VERIFIED | Prerequisite | No | Email not verified — handle on partner side |
PHONE_NOT_VERIFIED | Prerequisite | No | Phone not verified — handle on partner side |
ACTION_NOT_PERMITTED | Permission | No | Action denied for this user — message carries denyReason |
GEOFENCE_BLOCKED | Geofence | No | Restricted jurisdiction — user can't proceed |
TIMEOUT | Network | Yes | Request timed out after 15s |
NETWORK_ERROR | Network | Yes | Can't reach API — check connection |
UNCAUGHT_ERROR | Runtime | Yes | Unexpected embed error — retry or contact support |
Updated about 6 hours ago
