postMessage Protocol
Low-level reference for the PROPHETX_* postMessage events exchanged between your page and the ProphetX embed iframe.
When to use this guideIf you're using one of the ProphetX SDKs (
@prophetx/sdk-react,@prophetx/sdk-react-native, or@prophetx/sdk-js), the SDK handles all messaging for you. This guide is for partners who need direct iframe control — custom modal wrappers, non-JavaScript hosts, or advanced orchestration.
Message flow
A complete session follows this sequence:
Your page Embed (iframe)
| |
| <-- PROPHETX_EMBED_LOADED ---------- | iframe mounted
| |
| --- PROPHETX_SESSION --------------> | send JWT
| --- PROPHETX_THEME (optional) -----> | send theme
| |
| | validate token
| | (client + server)
| |
| <-- PROPHETX_READY ----------------- | session valid
| |
| <-- PROPHETX_RESIZE (repeated) ----- | content height changes
| |
| ... user completes flow ... |
| |
| <-- PROPHETX_SUCCESS --------------- | transaction complete
| or PROPHETX_ERROR | something went wrong
| or PROPHETX_CANCEL | user abandoned flow
| |
| --- (remove iframe) --- |
If the token expires mid-flow:
| <-- PROPHETX_SESSION_EXPIRED ------ | JWT exp reached
| |
| --- PROPHETX_SESSION_REFRESH -----> | send fresh JWT
| |
| ... flow resumes ... |
Outbound messages (Embed → Your page)
These are the messages your message event listener will receive from the embed.
PROPHETX_EMBED_LOADED
The iframe has mounted and is waiting for a session token. This is always the first message the embed sends.
{ "type": "PROPHETX_EMBED_LOADED" }When you receive this: respond immediately with PROPHETX_SESSION (and optionally PROPHETX_THEME).
PROPHETX_READY
The session token has been validated (client-side decode and server-side verification). The embed is fully initialized and the user can interact with it.
{ "type": "PROPHETX_READY" }When you receive this: the embed is live. Show it to the user if it was hidden during loading.
PROPHETX_SUCCESS
A deposit or withdrawal completed successfully.
{
"type": "PROPHETX_SUCCESS",
"payload": {
"transactionId": "tx_abc123",
"amount": 50.00
}
}| Field | Type | Description |
|---|---|---|
transactionId | string | Unique transaction identifier. Store this on your backend. |
amount | number | undefined | Dollar amount of the transaction. May be omitted in edge cases. |
When you receive this: close the embed, update your UI (refresh balance), and record the transactionId.
PROPHETX_ERROR
Something went wrong — a failed transaction, an invalid session, a geofence block, or an unexpected runtime error.
{
"type": "PROPHETX_ERROR",
"payload": {
"code": "DEPOSIT_FAILED",
"message": "The deposit could not be completed."
}
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code. See Error codes below. |
message | string | Human-readable description. Safe to display to the user. |
When you receive this: show an appropriate error state. Some codes (like SESSION_EXPIRED) have specific handling — see the error codes table.
PROPHETX_CANCEL
The user explicitly closed or cancelled the flow (e.g. tapped a close button inside the embed).
{ "type": "PROPHETX_CANCEL" }When you receive this: close the embed. No error occurred — the user chose to leave.
PROPHETX_RESIZE
The embed's content height changed. Sent whenever the visible content grows or shrinks — for example, when a form expands or an error banner appears.
{
"type": "PROPHETX_RESIZE",
"payload": {
"height": 580
}
}| Field | Type | Description |
|---|---|---|
height | number | Content height in pixels. A value of 0 means the embed has no content — use a default height (420px is recommended). |
When you receive this: update the iframe's height style to match. This prevents scrollbars inside the iframe and gives users a seamless experience.
PROPHETX_SESSION_EXPIRED
The JWT's exp claim has passed. The embed cannot continue until it receives a fresh token.
{ "type": "PROPHETX_SESSION_EXPIRED" }When you receive this: fetch a new JWT from your backend and send it back via PROPHETX_SESSION_REFRESH. If you cannot provide a fresh token, remove the iframe — the embed will remain in an expired state.
PROPHETX_GEOFENCE_BLOCKED
The user's location isn't eligible. Defined and wired identically on every flow, including 1-Click Signup — but it won't actually fire on 1-Click in practice, since that flow runs before a user exists and its tokenless session is treated as always-allowed rather than evaluated against the geo gate. In the current architecture, geo-eligibility is enforced when your backend mints the embed token rather than per-request, so this shouldn't fire in normal operation with a validly-issued token — but wire a handler for it defensively.
{
"type": "PROPHETX_GEOFENCE_BLOCKED",
"payload": {
"stateCode": "NY",
"reason": "Transactions are not available in your state."
}
}| Field | Type | Description |
|---|---|---|
stateCode | string | Two-letter US state code where the user was detected. |
reason | string | Human-readable explanation. Safe to display. |
When you receive this: show geo-specific messaging. The user cannot proceed from this state.
Identity verification: two separate message families
There are two distinct flows for identity verification, with different messages — they are not interchangeable:
- 1-Click Signup (new users,
useOneClickSignup) — Verified Inc's own hosted modal collects and shares an identity. KYC hasn't run yet at this point. - Document verification (existing users re-verifying after
FAILURE,useKyc/useIdUpload) — a government-ID photo upload reviewed by IDComply. This is the KYC decision.
PROPHETXONE_CLICK_SUCCESS / FAILED / _DECLINED
Terminal outcomes of 1-Click Signup.
{
"type": "PROPHETX_ONE_CLICK_SUCCESS",
"payload": { "identityUuid": "id_f47ac10b-58cc-4372-a567-0e02b2c3d479" }
}| Message | Payload | Meaning |
|---|---|---|
PROPHETX_ONE_CLICK_SUCCESS | { identityUuid: string } | User shared their identity. Always includes identityUuid — 1-Click always yields an identity when it succeeds. |
PROPHETX_ONE_CLICK_FAILED | { reason: string, retryable?: boolean } | Terminal failure. retryable is true only for a spent/lapsed session key — mint a fresh one and reopen. Everything else, fall back to manual signup. |
PROPHETX_ONE_CLICK_DECLINED | {} | The user chose not to share. Not an error — fall back to manual signup. |
This means "identity shared," not "KYC passed." KYC hasn't been asked for yet at this point — it only starts once your backend redeems the identity at
POST /private/v1/users/one-click. The resulting user can still resolve toFAILURE,MORTALITY,PEP, orOFAC. PollGET /private/v1/users/USERID/kyc-status(or listen for theKycStatusEventwebhook) for the real result, per User Onboarding.
When you receive _SUCCESS: redeem the identity server-side. When you receive _DECLINED: show your own manual signup form. When you receive _FAILED: check retryable — if true, mint a fresh session key and reopen; otherwise fall back to manual signup.
PROPHETXKYC_SUCCESS / FAILED / _PENDING
Terminal outcomes of document verification (useKyc/useIdUpload). Unlike 1-Click, this is the actual KYC decision — by the time one of these fires, the verdict is final (except _PENDING, which means review is still in progress).
{ "type": "PROPHETX_KYC_SUCCESS", "payload": {} }| Message | Payload | Meaning |
|---|---|---|
PROPHETX_KYC_SUCCESS | { identityUuid?: string } | Document verified. identityUuid is always absent — the IDPV API behind this flow returns none. You already have the user's UUID from POST /users. |
PROPHETX_KYC_FAILED | { reason: string, retryable?: boolean } | Rejected. retryable is false for compliance outcomes and exhausted attempts. reason is for logging/routing only — never display it. |
PROPHETX_KYC_PENDING | {} | Document submitted, review not yet resolved. |
When you receive this: for _SUCCESS, treat the user as verified — no further polling needed, this flow already waited for the verdict. For _FAILED, branch on retryable. For _PENDING, show a "still under review" state.
PROPHETX_HEADER_UPDATE
The embed's internal header changed — the title and back-button visibility for the current screen. Sent whenever the user navigates between steps inside a flow (e.g. amount → method selection → provider screen).
{
"type": "PROPHETX_HEADER_UPDATE",
"payload": {
"title": "Select payment method",
"showBack": true
}
}| Field | Type | Description |
|---|---|---|
title | string | Title text the embed wants to display in its header. |
showBack | boolean | Whether a back affordance should be visible for this screen. |
When you receive this: if you're rendering your own modal chrome around the iframe, update the title and toggle a back button accordingly. When the user taps your back button, send PROPHETX_BACK to the iframe (see below). If you let the embed render its own header, you can ignore this message — it's primarily for partners building custom chrome.
Inbound messages (Your page → Embed)
These are the messages you send to the embed iframe via iframe.contentWindow.postMessage(message, embedOrigin).
PROPHETX_SESSION
Delivers the signed JWT to the embed. Send this immediately after receiving PROPHETX_EMBED_LOADED.
{
"type": "PROPHETX_SESSION",
"payload": {
"token": "eyJhbGciOiJFZERTQSIs..."
}
}| Field | Type | Description |
|---|---|---|
token | string | A signed JWT. See Session Token Guide for claims and signing. |
The embed validates the token in two phases:
- Client-side — decodes the JWT and checks
exp - Server-side — sends the token to the ProphetX API for full validation (signature, issuer, expiration)
If both pass, the embed sends PROPHETX_READY. If either fails, it sends PROPHETX_ERROR with one of: SESSION_EXPIRED, SESSION_INVALID, or ORIGIN_UNAUTHORIZED.
PROPHETX_SESSION_REFRESH
Provides a fresh JWT after the embed reports PROPHETX_SESSION_EXPIRED. Only send this in response to an expiration — never proactively.
{
"type": "PROPHETX_SESSION_REFRESH",
"payload": {
"token": "eyJhbGciOiJFZERTQSIs..."
}
}| Field | Type | Description |
|---|---|---|
token | string | A freshly signed JWT replacing the expired one. |
The embed re-validates the new token and resumes the flow if valid.
PROPHETX_THEME
Configures the embed's visual appearance. Send alongside PROPHETX_SESSION on load, or at any time to update the theme dynamically (e.g. when the user toggles dark mode in your app).
{
"type": "PROPHETX_THEME",
"payload": {
"mode": "dark",
"primaryColor": "#e91e63"
}
}All fields are optional. The embed merges your overrides with its default design tokens.
| Field | Type | Description |
|---|---|---|
mode | "light" | "dark" | Base color scheme. |
primaryColor | string | Primary brand color (CSS color value). |
colorOverrides | object | Fine-grained color token overrides. See below. |
spacingOverrides | object | Spacing scale overrides (xs through xl). |
radiusOverrides | object | Border-radius overrides (sm, md, lg, full). |
fontSizeOverrides | object | Font-size scale overrides (xs through xl). |
All color override tokens
| Token | Description |
|---|---|
primary | Primary brand color |
primaryHover | Primary color on hover |
primaryForeground | Text on primary background |
secondary | Secondary brand color |
secondaryHover | Secondary color on hover |
background | Page background |
surface | Card/container background |
surfaceElevated | Elevated surface (modals, dropdowns) |
text | Primary text color |
textMuted | Secondary text color |
textSubtle | Tertiary/hint text color |
border | Default border color |
borderSubtle | Subtle/light border color |
error | Error text/icon color |
errorBackground | Error banner background |
success | Success text/icon color |
successBackground | Success banner background |
warning | Warning text/icon color |
warningBackground | Warning banner background |
cta | Call-to-action button background |
ctaHover | CTA button hover state |
ctaForeground | Text on CTA buttons |
filled | Filled button background |
filledHover | Filled button hover state |
filledForeground | Text on filled buttons |
danger | Danger/destructive action color |
dangerHover | Danger hover state |
dangerForeground | Text on danger buttons |
PROPHETX_LOCALE
Sets the language used inside the embed. Send alongside PROPHETX_SESSION on load, or at any time to switch languages dynamically (e.g. when the user changes locale in your app).
{
"type": "PROPHETX_LOCALE",
"payload": {
"locale": "es"
}
}| Field | Type | Description |
|---|---|---|
locale | string | BCP 47 locale tag. Currently supported: "en" (default), "es". Unknown locales fall back to "en". |
The embed swaps its UI strings as soon as the message is received. No restart or re-mount required.
PROPHETX_BACK
Tells the embed to navigate one step back in the current flow. Send this when the user taps a back button you render in your own modal chrome (in response to PROPHETX_HEADER_UPDATE with showBack: true).
{ "type": "PROPHETX_BACK" }If the embed manages its own header (the default), you don't need to send this — the back button inside the iframe handles it directly.
Error codes
These codes appear in the code field of PROPHETX_ERROR messages.
Session errors
| Code | Cause | What to do |
|---|---|---|
SESSION_EXPIRED | JWT exp is in the past | Fetch a fresh token and resend, or close the embed |
SESSION_INVALID | Malformed JWT or server-side rejection (bad signature, wrong issuer) | Check your signing implementation. Do not retry with the same token. |
ORIGIN_UNAUTHORIZED | Your page's origin is not in your partner-registered allowlist | Register your origin with ProphetX. See Before you begin. |
Deposit errors
| Code | Cause | What to do |
|---|---|---|
DEPOSIT_FAILED | Generic fallback — deposit API returned an error not covered by a provider-specific code | Display the message to the user. They can retry. |
PROVIDER_LOAD_FAILED / PROVIDER_UNAVAILABLE | The selected provider's widget failed to load, or isn't available right now | Display the message. The user can retry or pick a different provider. |
DEPOSIT_NOT_COMPLETED / DEPOSIT_WALLET_WRITE_FAILED | The deposit didn't finish, or the resulting wallet write failed | Display the message. Check transaction history before assuming funds were lost. |
ZEROHASH_LOAD_FAILED / ZEROHASH_PROVIDER_ERROR / ZEROHASH_DENIED / ZEROHASH_RESTRICTED / ZEROHASH_DETAILS_REQUIRED / ZEROHASH_NOT_APPROVED / ZEROHASH_KYC_PENDING / ZEROHASH_DEPOSIT_CONFLICT | ZeroHash-specific deposit failure modes | Display the message. Some (e.g. ZEROHASH_DETAILS_REQUIRED) mean the user needs to complete a step with ZeroHash before retrying. |
Real backend-level provider-specific codes (aeropay_rejected, pnm_rejected, amount_mismatch, etc.) are documented in Error Codes, but note they surface through the REST API, not through this PROPHETX_ERROR postMessage channel — the embed's own code here is limited to what's listed above. Trustly has no live embed endpoint today, so there is no TRUSTLY_* code to handle.
Withdrawal errors
| Code | Cause | What to do |
|---|---|---|
WITHDRAWAL_FAILED | Generic fallback — withdrawal API returned an error not covered by a provider-specific code | Display the message. The user can retry. |
KYC_REQUIRED | ⚠️ Real in the frontend SDK's withdraw flow, but its backend source is unconfirmed — see Error Codes before relying on it | If it fires, open useKyc/useIdUpload (this user already exists, so it's re-verification, not signup), then let the user retry the withdrawal. |
Other errors
| Code | Cause | What to do |
|---|---|---|
UNCAUGHT_ERROR | An unexpected runtime error in the embed | Log the message for debugging. Ask the user to retry or contact support. |
GEOFENCE_BLOCKED | The user's location is blocked (deposit/withdraw only, at transaction submit) | Handle the same as the dedicated PROPHETX_GEOFENCE_BLOCKED message below — this is a second, real delivery path for the same condition, not a stand-in for it. |
GEOFENCE_BLOCKED is also delivered via the dedicated PROPHETX_GEOFENCE_BLOCKED message / onGeofenceBlocked callback above — both channels are real and can fire for the same block, so handle either rather than assuming only one applies.
Complete example
A minimal integration without any SDK — just raw postMessage:
<button id="open-deposit">Deposit</button>
<div id="embed-container" style="display:none"></div>
<script>
const EMBED_URL = 'https://embed.prophetx.co/deposit';
const embedOrigin = new URL(EMBED_URL).origin;
let iframe;
document.getElementById('open-deposit').addEventListener('click', async () => {
// 1. Fetch a session token from your backend
const res = await fetch('/api/prophetx-token');
const { token } = await res.json();
// 2. Create the iframe
iframe = document.createElement('iframe');
iframe.src = EMBED_URL;
iframe.style.width = '100%';
iframe.style.height = '420px';
iframe.style.border = 'none';
const container = document.getElementById('embed-container');
container.innerHTML = '';
container.style.display = 'block';
container.appendChild(iframe);
// 3. Listen for messages from the embed
window.addEventListener('message', function handler(event) {
if (event.origin !== embedOrigin) return;
const { type, payload } = event.data;
switch (type) {
case 'PROPHETX_EMBED_LOADED':
// Respond with the session token
iframe.contentWindow.postMessage(
{ type: 'PROPHETX_SESSION', payload: { token } },
embedOrigin
);
// Optionally set a theme
iframe.contentWindow.postMessage(
{ type: 'PROPHETX_THEME', payload: { mode: 'light' } },
embedOrigin
);
break;
case 'PROPHETX_READY':
console.log('Embed is ready');
break;
case 'PROPHETX_RESIZE':
iframe.style.height = (payload.height || 420) + 'px';
break;
case 'PROPHETX_SUCCESS':
console.log('Transaction complete:', payload.transactionId, payload.amount);
cleanup(handler);
break;
case 'PROPHETX_ERROR':
console.error('Error:', payload.code, payload.message);
// Don't auto-close on SESSION_EXPIRED — refresh the token instead
if (payload.code === 'SESSION_EXPIRED') break;
cleanup(handler);
break;
case 'PROPHETX_CANCEL':
console.log('User cancelled');
cleanup(handler);
break;
case 'PROPHETX_SESSION_EXPIRED':
// Fetch a fresh token and send it back
fetch('/api/prophetx-token')
.then(r => r.json())
.then(({ token: freshToken }) => {
iframe.contentWindow.postMessage(
{ type: 'PROPHETX_SESSION_REFRESH', payload: { token: freshToken } },
embedOrigin
);
});
break;
}
});
});
function cleanup(handler) {
window.removeEventListener('message', handler);
document.getElementById('embed-container').style.display = 'none';
if (iframe) iframe.remove();
}
</script>Best practices
Always validate the origin. Only process messages where event.origin matches the embed URL's origin. Never use '*' as the targetOrigin when posting messages to the iframe — always use the embed's origin.
// Good
iframe.contentWindow.postMessage(message, 'https://embed.prophetx.co');
// Bad — any page could intercept this
iframe.contentWindow.postMessage(message, '*');Handle PROPHETX_RESIZE for a smooth UX. Without it, the iframe will show internal scrollbars as content changes. Set the iframe height to payload.height on every resize event, with a minimum of 420px.
Always handle session refresh. Tokens are short-lived (10 minutes by default). If a user takes time filling out a form, the token will expire. Listen for PROPHETX_SESSION_EXPIRED and respond with a fresh token to avoid interrupting the user's flow.
Clean up the event listener. Remove your message event listener when the flow ends (success, error, or cancel). Stale listeners can process messages from an old iframe if the user opens a second flow.
Updated 15 days ago
