Embedded UI (tokens + `/embed/v1`)
ProphetX provides drop-in UI components that handle trading and payment flows inside your frontend. They authenticate to ProphetX directly using a short-lived JWT that your backend mints for each user session, then call /embed/v1/* routes from the user's browser.
Your only responsibility on the backend is to mint those tokens. You don't call /embed/v1 yourself.
Auth on GET /tokens: user-scoped (sub + subsig) — see Authentication.
Which widgets does my integration need?
There are roughly four widgets available — Deposit, Withdraw, Onboarding, KYC. Which ones you embed depends on your fund type and how much of the user flow you want to own:
| Widget | INDIVIDUAL ISVs | STANDING ISVs |
|---|---|---|
| Deposit | Required. The private ISV API exposes no deposit endpoint — the only path money can enter a user's wallet is through this modal (calling /embed/v1/payment/*). | Not used for end-user funding (the standing fund is funded out-of-band). |
| Withdraw | Required. Same reason — there is no POST /withdraw on the private API; the modal is the only path out. | Not used for end-user payouts. |
| Onboarding | Optional. You can build your own onboarding UI on top of POST /users and the terms endpoints, or embed this component to let ProphetX walk the user through it. Most teams own onboarding themselves so they can match their brand and lifecycle. | |
| KYC | Optional. You can poll GET /users/USERID/kyc-status and render your own KYC state, or embed this widget to let ProphetX show the status and any retry/support affordances. |
For an INDIVIDUAL deployment, the practical minimum is: own onboarding + KYC display, embed Deposit + Withdraw (because there's no API alternative for money movement). Talk through the end-to-end user flow with your ProphetX contact before locking it in — there are a few sequencing edge cases (e.g. when to surface deposit prompts vs. KYC retries) that are easier to align on once rather than discovering after launch.
1. How the pieces fit together
Your frontend ProphetX-hosted embedded components
│ │
│ fetches a token │
├──► your backend │
│ │ │
│ │ GET /private/v1/tokens │
│ └──────────────────────────────►│
│ │
│ token + permissions + gates │
│◄──────────────────────────────────────│
│ │
│ init component with token │
├──► embedded UI │
│ │
│ │ /embed/v1/* calls
│ ├──► ProphetX API
│ │
│ │ validates token, returns
│ │◄──── responses
The token is the only ProphetX credential that ever touches the browser. It's short-lived and scoped to a single user session.
2. GET /private/v1/tokens
GET /private/v1/tokensUser-scoped. No body.
Response (TokenResponse):
{
"token": "eyJhbGciOi...",
"isvId": "00000000-0000-0000-0000-000000000000",
"userId": "00000000-0000-0000-0000-000000000000",
"expiration": "2026-05-12T12:05:00Z",
"permissions": {
"trade": { "granted": true, "description": "Can submit orders" },
"withdraw": { "granted": false, "description": "Can withdraw funds", "denyReason": "KYC pending" }
},
"gates": {
"kyc": { "completed": true, "description": "KYC verification" },
"terms": { "completed": false, "description": "Accept current terms" }
}
}What to do with each field:
| Field | Use |
|---|---|
token | Hand to the embedded UI component as its auth credential. Don't log it. |
expiration | Refresh before it expires; embedded components also surface refresh hooks. |
permissions | Map of permission key → { granted, description, denyReason? }. Drive your UI: only render trading controls if permissions.trade.granted. Show denyReason to explain a "no". |
gates | Onboarding gates the user must clear. Use these to drive an onboarding flow that walks them through any completed: false gate. |
permissions and gates are maps with arbitrary keys — new ones can appear without an API version bump. Don't hard-code an exhaustive switch; iterate the keys you got.
3. What /embed/v1/* exposes
/embed/v1/* exposesYou don't call these directly, but here's what the embedded UI is doing under the hood so you know what's reachable from a logged-in user's browser session.
| Method | Path | Purpose |
|---|---|---|
GET | /embed/v1/payment/methods | List available payment methods (proxies to the funds system). |
POST | /embed/v1/payment/init-provider | Initialize a payment provider session. |
POST | /embed/v1/payment/deposit | Start a deposit. |
POST | /embed/v1/payment/deposit-result | Finalize a deposit. |
POST | /embed/v1/payment/withdraw | Start a withdrawal. |
POST | /embed/v1/payment/withdraw-result | Finalize a withdrawal. |
GET | /embed/v1/terms | Same payload as /private/v1/terms. |
GET | /embed/v1/terms/USERID | Whether this user has accepted the current terms. |
POST | /embed/v1/terms/USERID | Record acceptance. |
GET | /embed/v1/wallet | The calling user's Wallet row. Always user-scoped — the token's sub decides whose wallet is returned, so the component cannot read another user's wallet or the ISV's standing fund. Same shape as /private/v1/wallets. |
GET | /embed/v1/token/validate | Validate an embed token and return the same TokenResponse payload. |
The terms routes are a straight passthrough to the private equivalents in User Onboarding (users, KYC, terms), so you can either handle T&Cs on your own backend or let the embedded UI handle them.
The payment routes are the only path for deposits and withdrawals — the private ISV API doesn't expose money-movement endpoints. Your frontend embeds the payment component, and the component calls /embed/v1/payment/* itself.
4. Token rotation
Tokens are short-lived (the expiration claim). Refresh them before they expire. A common pattern:
- On session start, fetch a token.
- Schedule a refresh just before
expiration. - When refreshing, fetch a new token from
GET /private/v1/tokensand pass it to the embedded component. - On the user logging out, drop the token. It can't be revoked from your side, but it will expire shortly.
Don't try to reuse a token across users or sessions — one token, one user, one short window.
5. Curl
BASE="https://isv-api.staging.prophetx.dev/private/v1"
# Mint a token for a user
curl "$BASE/tokens" -H "Authorization: Bearer $JWT_USER"The response goes back to your frontend over your own authenticated session with the user — not directly to the browser as a redirect from this API.
