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:

StackPackageInstall
React (web)@prophetx/sdk-reactnpm i @prophetx/sdk-react
Vanilla JS@prophetx/sdk-jsnpm i @prophetx/sdk-js
React Native@prophetx/sdk-react-nativenpm i @prophetx/sdk-react-native react-native-webview @react-native-async-storage/async-storage
Flutterprophetx_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

  1. What You Get
  2. Before You Start
  3. Quick Start (React)
  4. Quick Start (Vanilla JS)
  5. Quick Start (React Native)
  6. Quick Start (Flutter)
  7. Available Flows
  8. Built-In Modules
  9. Environments
  10. Lifecycle & Cleanup
  11. Error Codes
  12. API Reference Cheat Sheet
  13. FAQ & Troubleshooting

1. What You Get

Four end-user flows, fully implemented and hosted by ProphetX:

FlowWhat the user does
DepositPicks a payment method (Trustly, AeroPay, ZeroHash, Wire), enters an amount, and funds their ProphetX wallet.
WithdrawEnters an amount and withdraws to their bank via Trustly / AeroPay / ZeroHash.
OnboardingNew-user identity verification via Verified Inc.
KYCRe-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 environmentsproduction, staging, sandbox

2. Before You Start

Before you integrate the SDK:

  1. Create a ProphetX partner account — talk to ProphetX onboarding to get your sandbox/staging credentials.
  2. 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.
  3. 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-react

Wrap 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-js
import { 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 only

Peer 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 NSAppTransportSecurity is 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: unauthenticatedauthenticatingauthenticatedexpired → (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:

EventPayload
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

EnvironmentEmbed URLAPI Base URL
productionhttps://embed.prophetx.cohttps://api.prophetx.co
staginghttps://embed-staging.prophetx.cohttps://api-ss-staging.betprophet.co
sandboxhttps://embed-sandbox.prophetx.cohttps://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 through staging, then go live in production. 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 down

11. Error Codes

CodeWhereMeaning
DEPOSIT_FAILEDonError in depositGeneric deposit failure
DEPOSIT_NOT_COMPLETEDonError in depositUser exited without completing
TRUSTLY_LOAD_FAILEDonError in deposit/withdrawTrustly SDK failed to load
TRUSTLY_PROVIDER_ERRORonError in deposit/withdrawTrustly returned an error
ZEROHASH_LOAD_FAILEDonError in deposit/withdrawZeroHash failed to load
ZEROHASH_PROVIDER_ERRORonError in deposit/withdrawZeroHash returned an error
WITHDRAWAL_FAILEDonError in withdrawGeneric withdrawal failure
KYC_REQUIREDonError in withdrawUser must complete KYC first
SESSION_EXPIREDonErrorToken expired and no refresher provided
SESSION_INVALIDonErrorToken rejected by server
ORIGIN_UNAUTHORIZEDonErrorEmbedding origin isn't allow-listed
INVALID_CREDENTIALSauth.loginWrong email/password
NETWORK_ERRORauth.loginNetwork unreachable
SERVER_ERRORauth.login5xx from auth endpoint
TIMEOUTauth.loginRequest timed out

12. API Reference Cheat Sheet

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): void

Vanilla 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)

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)

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 pagehttps://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