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.
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 (Trustly, AeroPay, ZeroHash, Wire), enters an amount, and funds their ProphetX wallet. |
| Withdraw | Enters an amount and withdraws to their bank via Trustly / AeroPay / ZeroHash. |
| Onboarding | New-user identity verification via Verified Inc. |
| KYC | Re-verification flow for existing users. |
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 / openKyc / close / destroy / updateTheme / 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'),
onGeofenceBlocked:(state, reason) => showRegionMessage(state, reason),
});Possible error codes: DEPOSIT_FAILED, TRUSTLY_LOAD_FAILED, TRUSTLY_PROVIDER_ERROR, ZEROHASH_LOAD_FAILED, ZEROHASH_PROVIDER_ERROR, DEPOSIT_NOT_COMPLETED.
Withdraw
const { open } = useWithdraw();
open({
onSuccess: (txId, amount) => console.log('Withdrew', txId, amount),
onError: (code, msg) => {
if (code === 'KYC_REQUIRED') {
// Send the user through onboarding first
onboarding.open();
}
},
});Possible error codes: WITHDRAWAL_FAILED, KYC_REQUIRED.
Onboarding (new-user KYC)
Use this the first time a user transacts. Verified Inc handles the document scan.
const { open } = useOnboarding();
open({
onKycSuccess: (identityUuid) => console.log('KYC verified', identityUuid),
onKycFailed: (reason) => console.warn('KYC failed', reason),
onKycPending: () => toast('Identity check pending review'),
});KYC Verification (existing user)
Used for re-verification (annual checks, suspicious activity, etc.). Same callbacks as onboarding.
const { open } = useKyc();
open({ onKycSuccess: handleSuccess });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://api-ss-staging.betprophet.co',
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://api-ss-staging.betprophet.co' },
});
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://api.prophetx.co |
staging | https://embed-staging.prophetx.co | https://api-ss-staging.betprophet.co |
sandbox | https://embed-sandbox.prophetx.co | https://api-ss-sandbox.betprophet.co |
// 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 |
DEPOSIT_NOT_COMPLETED | onError in deposit | User exited without completing |
TRUSTLY_LOAD_FAILED | onError in deposit/withdraw | Trustly SDK failed to load |
TRUSTLY_PROVIDER_ERROR | onError in deposit/withdraw | Trustly returned an error |
ZEROHASH_LOAD_FAILED | onError in deposit/withdraw | ZeroHash failed to load |
ZEROHASH_PROVIDER_ERROR | onError in deposit/withdraw | ZeroHash returned an error |
WITHDRAWAL_FAILED | onError in withdraw | Generic withdrawal failure |
KYC_REQUIRED | onError in withdraw | User must complete KYC first |
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 |
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 }
useOnboarding(): { open(opts?), close(), isOpen }
useKyc(): { open(opts?), close(), isOpen }
// 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;
openKyc(opts): void;
close(): void;
destroy(): void;
updateTheme(theme): 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
- 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
