Frontend Integration Guide
Embeddable deposit, withdraw, onboarding, and KYC flows for ISV partners.
Add ProphetX-hosted deposit, withdrawal, onboarding, and KYC flows to your frontend using the SDK for your stack.
ProphetX provides embeddable deposit, withdraw, onboarding, and KYC flows for ISV partners. Each SDK works like a drop-in that you wrap around your app and call from a button.
See it live: isv-embed-fe-mock-isv-host.vercel.app is a demo ISV host app running all four flows end-to-end against sandbox — a working reference before you write any code.
There are four SDKs — pick the one that matches your stack:
| Stack | Package | Install |
|---|---|---|
| React (web) | @prophetx/sdk-react | npm i @prophetx/sdk-react |
| Vanilla JS | @prophetx/sdk-js | npm i @prophetx/sdk-js |
| React Native | @prophetx/sdk-react-native | npm i @prophetx/sdk-react-native react-native-webview @react-native-async-storage/async-storage |
| Flutter | prophetx_sdk (Dart) | Add to pubspec.yaml (see Flutter) |
All four expose the same set of flows, events, and configuration options — only the rendering primitive differs (iframe vs. WebView).
Table of Contents
- What You Get
- Before You Start
- Quick Start (React)
- Quick Start (Vanilla JS)
- Quick Start (React Native)
- Quick Start (Flutter)
- Available Flows
- Built-In Modules
- Environments
- Lifecycle & Cleanup
- Error Codes
- API Reference Cheat Sheet
- FAQ & Troubleshooting
1. What You Get
Four end-user flows, fully implemented and hosted by ProphetX:
| Flow | What the user does |
|---|---|
| Deposit | Picks a payment method (AeroPay, ZeroHash, or Wire Transfer — PayNearMe is API-supported but has no shipped embed flow yet, and shows a placeholder if granted), enters an amount, and funds their ProphetX wallet. |
| Withdraw | Enters an amount and withdraws to their bank via AeroPay (ZeroHash and PayNearMe withdraw have no shipped embed flow yet). |
| 1-Click Signup | New-user identity creation via Verified Inc's own hosted modal. |
| Document verification | Re-verification flow for existing users whose automated KYC came back FAILURE. |
Each flow opens in a modal (web) or a full-screen sheet (mobile). You get callbacks on success, error, cancel, and KYC outcomes, plus a global event stream for analytics.
Out of the box you also get:
- Built-in authentication with token storage and auto-refresh
- Wallet balance API
- Theming — match the embed to your brand at open time or dynamically
- Geofence handling — surface region/VPN blocks to your UI
- DevTools — toggle-on debug logger with automatic PII redaction
- Three environments —
production,staging,sandbox
2. Before You Start
Before you integrate the SDK:
- Create a ProphetX partner account — talk to ProphetX onboarding to get your sandbox/staging credentials.
- Provide a session JWT for each user — use either the built-in auth module (recommended) or your own backend that calls ProphetX's auth API.
- Allow-list your origins — coordinate with ProphetX before going live so your production domains can render the embed.
The SDK never asks you to embed payment provider SDKs (Trustly, AeroPay, ZeroHash) directly — those all live inside the ProphetX-hosted embed.
3. Quick Start (React)
npm i @prophetx/sdk-reactWrap your app once, then call hooks anywhere inside.
import { ProphetXProvider, useDeposit, useWithdraw } from '@prophetx/sdk-react';
function App() {
return (
<ProphetXProvider
environment="production"
token={sessionJwt} // get this from your backend, or use auth={...} (see below)
>
<WalletButtons />
</ProphetXProvider>
);
}
function WalletButtons() {
const deposit = useDeposit();
const withdraw = useWithdraw();
return (
<>
<button onClick={() => deposit.open({
onSuccess: (txId, amount) => console.log('Deposited', txId, amount),
onError: (code, msg) => console.error('Deposit failed', code, msg),
onCancel: () => console.log('User cancelled'),
})}>
Deposit
</button>
<button onClick={() => withdraw.open({
onSuccess: (txId, amount) => console.log('Withdrew', txId, amount),
onError: (code, msg) => console.error('Withdrawal failed', code, msg),
})}>
Withdraw
</button>
</>
);
}You now have a basic deposit and withdrawal button wired to ProphetX-hosted flows.
4. Quick Start (Vanilla JS)
npm i @prophetx/sdk-jsimport { createProphetX } from '@prophetx/sdk-js';
const px = createProphetX({ environment: 'production' });
document.querySelector('#deposit-btn').addEventListener('click', () => {
px.openDeposit({
token: sessionJwt,
onSuccess: (txId, amount) => console.log('Deposited', txId, amount),
onError: (code, msg) => console.error(code, msg),
onCancel: () => console.log('User cancelled'),
});
});
// Clean up when your page tears down
window.addEventListener('beforeunload', () => px.destroy());createProphetX returns an SDKInstance with openDeposit / openWithdraw / openOnboarding (gated — see warning below) / openOneClickSignup / openKyc (or openIdUpload) / close / destroy / updateTheme / updateLocale / on / off / auth / wallet.
5. Quick Start (React Native)
npm i @prophetx/sdk-react-native react-native-webview @react-native-async-storage/async-storage
cd ios && pod install # iOS onlyPeer dependencies: react ^18, react-native >=0.70, react-native-webview >=13, @react-native-async-storage/async-storage >=1.19.
import { ProphetXProvider, useDeposit } from '@prophetx/sdk-react-native';
export default function App() {
return (
<ProphetXProvider environment="production" token={sessionJwt}>
<Home />
</ProphetXProvider>
);
}
function Home() {
const { open } = useDeposit();
return <Button title="Deposit" onPress={() => open({
onSuccess: (txId, amount) => console.log('Deposited', txId, amount),
onError: (code, msg) => console.error('Deposit failed', code, msg),
})} />;
}The provider mounts a full-screen modal with header + WebView when you open a flow. You don't render the WebView yourself — but a low-level ProphetXWebView is exported if you need custom chrome.
iOS/Android setup: make sure your app has Internet permission (Android) and that
NSAppTransportSecurityis not blocking HTTPS (iOS).
6. Quick Start (Flutter)
Add to pubspec.yaml:
dependencies:
prophetx_sdk:
git:
url: [email protected]:betprophet1/isv-embed-fe.git
path: packages/sdk-flutter
ref: flutter-sdk-v0.1.0 # use the latest published tag(Once GA, it will be available as prophetx_sdk: ^x.y.z from pub.dev.)
import 'package:flutter/material.dart';
import 'package:prophetx_sdk/prophetx_sdk.dart';
void main() {
runApp(
ProphetXProvider(
environment: EnvironmentName.production,
token: sessionJwt,
child: const MyApp(),
),
);
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final px = ProphetX.of(context);
return ElevatedButton(
onPressed: () => px.deposit.open(OpenDepositOptions(
onSuccess: (id, amount) => debugPrint('deposit success $id'),
onError: (code, message) => debugPrint('deposit failed $code: $message'),
)),
child: const Text('Deposit'),
);
}
}Native deps the SDK pulls in: webview_flutter, flutter_secure_storage, http, uuid. Android cleartext is not required for production/staging since the API base URLs are HTTPS.
7. Available Flows
Every flow follows the same pattern:
flow.open({
// Lifecycle callbacks
onReady?,
onSuccess?,
onError?,
onCancel?,
// KYC / geofence callbacks (when applicable)
onKycSuccess?, onKycFailed?, onKycPending?,
onGeofenceBlocked?,
// Per-invocation theme override
theme?,
// Manual session refresh (not needed if you use the auth module)
onSessionExpired?,
});Deposit
User picks method → enters amount → confirms → completes via provider.
// React
const { open } = useDeposit();
open({
onSuccess: (txId, amount) => track('deposit_success', { txId, amount }),
onError: (code, msg) => track('deposit_error', { code, msg }),
onCancel: () => track('deposit_cancel'),
});Possible error codes: DEPOSIT_FAILED (generic fallback), PROVIDER_LOAD_FAILED, PROVIDER_UNAVAILABLE, DEPOSIT_NOT_COMPLETED, DEPOSIT_WALLET_WRITE_FAILED, plus ZeroHash-specific codes — ZEROHASH_LOAD_FAILED, ZEROHASH_PROVIDER_ERROR, ZEROHASH_DENIED, ZEROHASH_RESTRICTED, ZEROHASH_DETAILS_REQUIRED, ZEROHASH_NOT_APPROVED, ZEROHASH_KYC_PENDING, ZEROHASH_DEPOSIT_CONFLICT — see Error Codes. onGeofenceBlocked(stateCode, reason) is also wired on this flow, though in practice geo-eligibility is enforced when your backend mints the embed token, not per-request — see the geofence FAQ entry in FAQ & Troubleshooting.
Withdraw
const { open } = useWithdraw();
open({
onSuccess: (txId, amount) => console.log('Withdrew', txId, amount),
onError: (code, msg) => {
if (code === 'KYC_REQUIRED') {
// This user already exists (they have an embed token) — re-verify them,
// don't send them through signup.
idUpload.open();
}
},
});Possible error codes: WITHDRAWAL_FAILED (generic fallback). Withdraw may also surface a literal KYC_REQUIRED per the SDK's own source — its backend origin is unconfirmed (no backend code path we could find actually emits it, so treat it as a legacy/defensive client-side code rather than something you're likely to see in practice), see Error Codes.
1-Click Signup (new-user identity)
useOnboarding/openOnboarding— previously documented here — is not available: the route is permanently gated off with an error screen (the underlying Verified Inc API integration was never approved for that integration type). UseuseOneClickSignupfor new users instead.
Use this the first time a brand-new user signs up — there's no ProphetX user yet, so there's no embed token either. Verified Inc's own hosted modal handles phone/OTP lookup and PII confirmation; your backend mints a sessionKey first and redeems the resulting identity afterward. KYC hasn't run yet at that point — it starts once your backend redeems the identity, and can still come back FAILURE. See Verified Modal for the full contract (sessionKey/environment, onOneClickSuccess/onOneClickDeclined/onOneClickFailed).
Document verification (existing-user re-verification)
For an existing user whose automated KYC came back FAILURE, use useKyc (historical alias) or useIdUpload (preferred) — a government-ID photo upload reviewed by IDComply, opened in a second browser tab. There's no phone/OTP step in this flow. See Verified Modal for the full walkthrough.
const { open } = useOneClickSignup();
const { sessionKey, environment } = await myBackend.mintOneClickSession();
open({
sessionKey,
environment,
onOneClickSuccess: (identityUuid) => myBackend.redeem(identityUuid),
onOneClickDeclined: () => showManualSignupForm(),
onOneClickFailed: (reason, retryable) => console.warn('1-Click failed', reason, retryable),
});const { open: idUpload } = useIdUpload(); // or useKyc — same flow
idUpload.open({
onKycSuccess: () => handleSuccess(),
onKycFailed: (reason, retryable) => console.warn('Verification failed', reason, retryable),
onKycPending: () => toast('Identity check pending review'),
});8. Built-In Modules
Authentication
You have two options for token management:
Option A — let the SDK do it (recommended). Pass an auth config; the SDK handles login, storage, JWT decoding, and auto-refresh.
// React
<ProphetXProvider
environment="staging"
auth={{
apiBaseUrl: 'https://isv-api.staging.prophetx.dev',
refreshBufferSeconds: 30, // auto-refresh 30s before expiry
}}
>
<App />
</ProphetXProvider>
function Login() {
const { login, logout, isAuthenticated, state } = useAuth();
if (!isAuthenticated) {
return (
<button onClick={() => login({ email: '[email protected]', password: 'pw' })}>
Sign in
</button>
);
}
return <button onClick={logout}>Sign out (expires {state.expiresAt})</button>;
}// Vanilla JS
const px = createProphetX({
environment: 'staging',
auth: { apiBaseUrl: 'https://isv-api.staging.prophetx.dev' },
});
await px.auth.login({ email, password });
px.auth.onStateChange((state) => console.log('Auth →', state.status));
px.auth.logout();Auth states: unauthenticated → authenticating → authenticated → expired → (auto-refresh) → authenticated. Login errors land in error with codes INVALID_CREDENTIALS, NETWORK_ERROR, SERVER_ERROR, TIMEOUT.
Device ID is auto-generated (crypto.randomUUID()) and persisted (localStorage on web, AsyncStorage on RN, flutter_secure_storage on Flutter). You can override with login({ email, password, deviceId }).
Option B — bring your own token. Pass token as a prop / option, and (optionally) an onSessionExpired handler that returns a fresh token. See Session refresh below.
Wallet / Balance
Once authenticated, you can fetch the user's balance.
// React
const { balance, loading, error, refetch } = useWallet();
return loading ? <Spinner /> : <p>${balance?.totalBalance}</p>;// Vanilla JS
const { totalBalance, currency } = await px.wallet.getBalance();Response: { totalBalance: number, currency: string } (default currency 'cash').
Theming
Match the embed to your brand. Two ways to set a theme:
// 1. On the provider (default for all flows)
<ProphetXProvider
environment="production"
token={jwt}
theme={{
mode: 'dark',
primaryColor: '#4F46E5',
backgroundColor: '#1a1a2e',
colorOverrides: { 'text-primary': '#fff' },
radiusOverrides: { sm: '4px', md: '8px', lg: '16px' },
}}
>...// 2. Per-invocation override
px.openDeposit({ theme: { mode: 'light' } });Updating dynamically:
// Vanilla JS
px.updateTheme({ mode: 'light' });// React — just change the prop, the provider re-sends automatically
const [theme, setTheme] = useState({ mode: 'light' });
<ProphetXProvider theme={theme} ...>...</ProphetXProvider>Full ThemePayload shape:
type ThemePayload = {
mode?: 'light' | 'dark';
primaryColor?: string;
backgroundColor?: string;
colorOverrides?: Record<string, string>;
spacingOverrides?: Record<string, string>;
radiusOverrides?: Record<string, string>;
fontSizeOverrides?: Record<string, string>;
};Events
Subscribe once and react to anything the embed emits — perfect for analytics or error reporting.
// React
import { useEvents } from '@prophetx/sdk-react';
function Analytics() {
useEvents({
onReady: () => analytics.track('embed_ready'),
onSuccess: (d) => analytics.track('flow_success', d),
onError: (d) => errorReporter.capture('flow_error', d),
onCancel: () => analytics.track('flow_cancel'),
onGeofenceBlocked:(d) => analytics.track('geofence_block', d),
onKycSuccess: (d) => analytics.track('kyc_success', d),
onKycFailed: (d) => analytics.track('kyc_failed', d),
onKycPending: () => analytics.track('kyc_pending'),
});
return null;
}// Vanilla JS
const unsub = px.on('success', (d) => analytics.track('deposit_success', d));
px.on('error', (d) => errorReporter.capture('sdk_error', d));
px.on('open', (d) => analytics.track('flow_open', { flow: d.flow }));
px.on('close', (d) => analytics.track('flow_close', { flow: d.flow }));
unsub();All events:
| Event | Payload |
|---|---|
ready | — |
success | { transactionId, amount? } |
error | { code, message } |
cancel | — |
kycSuccess | { identityUuid } |
kycFailed | { reason } |
kycPending | — |
geofenceBlocked | { stateCode, reason } |
resize | { height } |
sessionExpired | — |
sessionRefreshed | { token } |
open / close | { flow: 'deposit' | 'withdraw' | ... } |
Geofence
US betting regulation varies by state. If the user is in a blocked region (or behind a VPN), you'll get a callback so you can swap in your own messaging.
deposit.open({
onGeofenceBlocked: (stateCode, reason) => {
// reason: 'region' | 'vpn' | 'error'
if (reason === 'vpn') showVpnWarning();
else showStateUnavailable(stateCode);
},
});You can also subscribe globally via on('geofenceBlocked', ...) / useEvents({ onGeofenceBlocked }).
Session refresh
Using the auth module: session expiry is handled automatically — when the embed reports PROPHETX_SESSION_EXPIRED, the SDK re-authenticates using stored credentials and the flow resumes. You don't need to do anything.
Without the auth module: provide an onSessionExpired handler that returns a fresh token (or null to abort):
<ProphetXProvider
environment="production"
token={jwt}
onSessionExpired={async () => {
const res = await fetch('/api/auth/refresh');
const data = await res.json();
return data.token; // return null to abort
}}
>...px.openDeposit({
token: jwt,
onSessionExpired: async () => (await refresh()).token,
});DevTools
Toggle-on debug logging with PII redaction. Off by default.
<ProphetXProvider
environment="staging"
devtools={{
enabled: true,
logLevel: 'debug', // 'none' | 'error' | 'warn' | 'info' | 'debug'
redactPII: true, // default true; masks tokens, emails, passwords
onEvent: (entry) => myLogger.log(entry.category, entry.message, entry.data),
}}
>...Categories: auth (purple), message (blue), api (green), lifecycle (amber), event (pink). With redactPII on, tokens / passwords / emails appear as [REDACTED] in logs.
9. Environments
| Environment | Embed URL | API Base URL |
|---|---|---|
production | https://embed.prophetx.co | https://isv-api.prophetx.co |
staging | https://embed-staging.prophetx.co | https://isv-api.staging.prophetx.dev |
sandbox | https://embed-sandbox.prophetx.co | https://isv-api.sandbox.prophetx.dev |
// Preset (recommended)
createProphetX({ environment: 'staging' });
<ProphetXProvider environment="staging" .../>
// Custom (point at your own deploys)
createProphetX({
environment: { embedUrl: 'https://embed.example.com', apiBaseUrl: 'https://api.example.com' },
});Start in
sandbox, promote throughstaging, then go live inproduction. Your allow-listed origins must be configured by ProphetX before production traffic will work.
10. Lifecycle & Cleanup
React / React Native / Flutter: cleanup is automatic when the provider unmounts. Nothing extra to do.
Vanilla JS: call destroy() when you're done with the SDK instance (typically on page unload). This tears down the iframe, removes listeners, and stops auth timers. After destroy() the instance is unusable.
px.close(); // close the active flow modal
px.destroy(); // tear the whole SDK down11. Error Codes
| Code | Where | Meaning |
|---|---|---|
DEPOSIT_FAILED | onError in deposit | Generic deposit failure (fallback — check for a provider-specific code first) |
WITHDRAWAL_FAILED | onError in withdraw | Generic withdrawal failure (fallback) |
PERMISSION_DENIED | onError in deposit/withdraw | A required gate is unmet, or a provider permission is denied — check pending_gates / denyReason. Covers what used to be documented as TERMS_NOT_SIGNED-style codes; there's no Trustly-specific code (not wired to any live endpoint). |
KYC_REQUIRED | onError in withdraw only | ⚠️ Real in the frontend SDK's source, but unconfirmed against isv-backend — see Error Codes. |
onGeofenceBlocked | Dedicated callback, not onError | Real, on every flow except 1-Click Signup — but enforced at token issuance, so it shouldn't fire with a validly-issued token. |
SESSION_EXPIRED | onError | Token expired and no refresher provided |
SESSION_INVALID | onError | Token rejected by server |
ORIGIN_UNAUTHORIZED | onError | Embedding origin isn't allow-listed |
INVALID_CREDENTIALS | auth.login | Wrong email/password |
NETWORK_ERROR | auth.login | Network unreachable |
SERVER_ERROR | auth.login | 5xx from auth endpoint |
TIMEOUT | auth.login | Request timed out |
See Error Codes for the full reference, including the real provider-specific codes (aeropay_rejected, pnm_rejected, amount_mismatch, etc.) that a DEPOSIT_FAILED/WITHDRAWAL_FAILED fallback may be masking.
12. API Reference Cheat Sheet
React (@prophetx/sdk-react)
@prophetx/sdk-react)<ProphetXProvider
environment? : 'production' | 'staging' | 'sandbox' | EnvironmentConfig
token? : string // omit when using `auth`
theme? : ThemePayload
auth? : { apiBaseUrl: string, refreshBufferSeconds?: number }
devtools? : DevToolsConfig
onSessionExpired? : () => Promise<string | null>
>
// Flow hooks
useDeposit(): { open(opts?), close(), isOpen }
useWithdraw(): { open(opts?), close(), isOpen }
useOneClickSignup(): { open(opts), close(), isOpen } // opts required — needs sessionKey
useKyc() / useIdUpload(): { open(opts?), close(), isOpen } // same flow, two names
// Module hooks
useAuth(): { state, login, logout, token, isAuthenticated }
useWallet(): { balance, loading, error, refetch }
useEvents(handlers): voidVanilla JS (@prophetx/sdk-js)
@prophetx/sdk-js)createProphetX({ environment?, auth?, devtools? }): SDKInstance
type SDKInstance = {
openDeposit(opts): void;
openWithdraw(opts): void;
openOnboarding(opts): void; // opens the gated error screen — see the 1-Click Signup warning above
openOneClickSignup(opts): void; // opts required — needs sessionKey/environment
openKyc(opts): void; // historical alias
openIdUpload(opts): void; // same flow as openKyc, preferred name
close(): void;
destroy(): void;
updateTheme(theme): void;
updateLocale(locale): void;
on(event, handler): () => void;
off(event, handler): void;
auth: AuthManager | null; // null until `auth` configured
wallet: WalletClient | null;
};Options shared by all flows
type OpenOptions = {
token?: string; // not needed with auth module
theme?: ThemePayload;
onReady?: () => void;
onSuccess?: (transactionId: string, amount?: number) => void;
onError?: (code: string, message: string) => void;
onCancel?: () => void;
onGeofenceBlocked?: (stateCode: string, reason: string) => void;
onSessionExpired?: () => Promise<string | null>;
};
type KycOpenOptions = OpenOptions & {
onKycSuccess?: (identityUuid: string) => void;
onKycFailed?: (reason: string) => void;
onKycPending?: () => void;
};React Native (@prophetx/sdk-react-native)
@prophetx/sdk-react-native)Same API as @prophetx/sdk-react. Provider renders a full-screen native Modal with WebView. ProphetXWebView and ProphetXModal are exported for advanced custom chrome.
Flutter (prophetx_sdk)
prophetx_sdk)ProphetXProvider(
environment: EnvironmentName.production, // or EnvironmentConfig(...)
token: jwt,
auth: AuthConfig(apiBaseUrl: '...'),
theme: const EmbedTheme(mode: 'dark'),
locale: 'en',
child: const MyApp(),
);
final px = ProphetX.of(context);
px.deposit.open(OpenDepositOptions(onSuccess: ...));
px.withdraw.open(...);
px.onboarding.open(...);
px.kyc.open(...);
px.auth?.login(LoginCredentials(email: ..., password: ...));
px.wallet?.refetch();
px.events.stream.listen((event) { ... }); // ReadyEvent | SuccessEvent | ...13. FAQ & Troubleshooting
Q: Do I need to embed Trustly / AeroPay / ZeroHash SDKs?
No. The ProphetX embed loads them itself.
Q: The embed isn't rendering — I get a blank modal.
Most common cause: your origin isn't on ProphetX's allow-list. Contact your ProphetX integration contact and confirm window.location.origin. In dev, also confirm you're pointing at the right environment.
Q: I keep getting SESSION_EXPIRED.
Either turn on the auth module (auth: { apiBaseUrl }) so the SDK refreshes for you, or implement onSessionExpired to return a fresh token from your backend.
Q: How do I detect when the user closed the modal vs. completed?
Use onCancel (user dismissed) vs. onSuccess (flow completed). They're mutually exclusive per open() call.
Q: Can I show my own loading state before the embed shows?
Yes — keep your own spinner up until onReady fires.
Q: How do I customize the modal chrome on mobile?
For RN, the default ProphetXProvider renders ProphetXModal for you. To swap in your own header / presentation, use the lower-level ProphetXWebView directly. Flutter has the same pattern with ProphetXWebView.
Q: What if my user is on a VPN?
You'll get onGeofenceBlocked(stateCode, 'vpn'). Surface an appropriate message — don't auto-retry.
Q: Where do I send sensitive analytics — through useEvents or my own backend?
Either, but useEvents already passes you only the event payloads (no raw tokens or PII). The built-in DevTools also redact PII before logging.
Q: How do I switch the theme to match a dark/light toggle in my app?
Pass a state variable to theme on the provider (React) or call px.updateTheme(...) (Vanilla JS). The embed updates live.
Q: Can I run two flows at once?
No — opening a second flow closes the first one. This is intentional to keep the user's attention on one transaction at a time.
Support resources
- Live demo — isv-embed-fe-mock-isv-host.vercel.app, a mock ISV host app with all flows wired to sandbox
- Status page —
https://status.prophetx.co(ask your contact for the right URL for your tenant) - Partner support — reach out through your ProphetX integration channel
- SDK source — available on request for security review
Updated 15 days ago
