Error Codes

How errors are delivered

ChannelWhenWhat you receive
onError(code, message)A deposit, withdrawal, or session operation failsA machine-readable code and a human-readable message safe to display
PROPHETX_SESSION_EXPIREDThe JWT's exp claim has passedA standalone event — respond with a fresh token
PROPHETX_GEOFENCE_BLOCKEDUser's location is restrictedstateCode and reason
PROPHETX_KYC_FAILEDIdentity verification was rejectedreason 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) => void

If 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.

CodeHTTPRetryableCause
SESSION_EXPIREDYesThe JWT's exp claim is in the past
SESSION_INVALID401NoMalformed JWT, bad signature, wrong issuer, or server-side rejection
ORIGIN_UNAUTHORIZED403NoYour page's origin is not in your partner-registered allowlist

SESSION_EXPIRED

The 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

The 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 EdDSA algorithm with an Ed25519 private key
  • Confirm the iss claim matches your ISV ID
  • Confirm the sub claim is a valid ProphetX user ID
  • Ensure the token hasn't been tampered with after signing

ORIGIN_UNAUTHORIZED

The 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 active

These 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.

CodeSource gateRetryableCause
TERMS_NOT_SIGNEDgates.termsNoUser has not accepted ProphetX's terms and conditions
EMAIL_NOT_VERIFIEDgates.email-verifyNoUser's email address has not been verified
PHONE_NOT_VERIFIEDgates.phone-verifyNoUser's phone number has not been verified
GEOFENCE_BLOCKEDgates.geoipNoUser'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.completed is always true for any user who has a session token. You will not see a KYC-related prerequisite error from the validate gates. (Note: KYC_REQUIRED may still fire on a withdraw attempt — that is a separate withdrawal-specific gate; see Withdrawal errors.)

EMAIL_NOT_VERIFIED

The 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

The 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

The 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 useOnboarding to 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 finalizing

The validate response includes a permissions object 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.

CodeRetryableCause
ACTION_NOT_PERMITTEDNoThe 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.

CodeRetryableCause
DEPOSIT_FAILEDYesThe deposit API returned an error during initiation or submission
DEPOSIT_NOT_COMPLETEDNoDeposit was initiated but finalization returned a non-completed status
TRUSTLY_LOAD_FAILEDYesThe Trustly payment widget failed to load
TRUSTLY_PROVIDER_ERRORYesTrustly returned an error during the payment flow
ZEROHASH_PROVIDER_ERRORYesZeroHash returned an error during the payment flow

DEPOSIT_FAILED

The 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

The 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

The 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'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 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.

CodeRetryableCause
WITHDRAWAL_FAILEDYesThe withdrawal API returned an error
KYC_REQUIREDNoUser has not completed identity verification

WITHDRAWAL_FAILED

The 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

The 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_STARTED will cover this case for both deposits and withdrawals. We recommend handling both codes now.

Geofence errors

CodeRetryableCause
GEOFENCE_BLOCKEDNoUser is in a US state where transactions are not permitted

GEOFENCE_BLOCKED

The user's geolocation was checked before a deposit or withdrawal, and they're in a restricted jurisdiction.

This error fires through two channels simultaneously:

  1. onError("GEOFENCE_BLOCKED", message) — the standard error callback
  2. PROPHETX_GEOFENCE_BLOCKED postMessage — a dedicated event with stateCode and reason

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

CodeRetryableCause
UNCAUGHT_ERRORYesAn unexpected runtime error inside the embed

UNCAUGHT_ERROR

A 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.

CodeRetryableCause
TIMEOUTYesThe API request exceeded the timeout threshold (15 seconds)
NETWORK_ERRORYesThe browser couldn't reach the API (DNS failure, offline, CORS block)

TIMEOUT

A 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

The 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.

CodeCategoryRetryableSummary
SESSION_EXPIREDSessionYesJWT expired — refresh the token
SESSION_INVALIDSessionNoBad JWT — check signing implementation
ORIGIN_UNAUTHORIZEDSessionNoOrigin not registered — contact ProphetX
DEPOSIT_FAILEDDepositYesDeposit API error — user can retry
DEPOSIT_NOT_COMPLETEDDepositNoDeposit pending — check status, don't retry
TRUSTLY_LOAD_FAILEDDepositYesTrustly widget didn't load — network issue
TRUSTLY_PROVIDER_ERRORDepositYesTrustly error during payment — user can retry
ZEROHASH_PROVIDER_ERRORDepositYesZeroHash error — user can retry
WITHDRAWAL_FAILEDWithdrawalYesWithdrawal API error — user can retry
KYC_REQUIREDWithdrawalNoWithdrawal-specific KYC gate — open onboarding
TERMS_NOT_SIGNEDPrerequisiteNoT&C not accepted — open useTerms
EMAIL_NOT_VERIFIEDPrerequisiteNoEmail not verified — handle on partner side
PHONE_NOT_VERIFIEDPrerequisiteNoPhone not verified — handle on partner side
ACTION_NOT_PERMITTEDPermissionNoAction denied for this user — message carries denyReason
GEOFENCE_BLOCKEDGeofenceNoRestricted jurisdiction — user can't proceed
TIMEOUTNetworkYesRequest timed out after 15s
NETWORK_ERRORNetworkYesCan't reach API — check connection
UNCAUGHT_ERRORRuntimeYesUnexpected embed error — retry or contact support