User Onboarding (users, KYC, terms)
Before a user can place a trade, three things need to happen:
- You create the user record.
- The user accepts the current terms.
- KYC resolves successfully.
These can happen in roughly any order, but trading is gated on all three. This doc walks through each one.
Auth: every route here requires a JWT (see Authentication). The create route uses an ISV-only JWT; everything else is user-scoped, meaning sub must equal the USERID in the URL and you need subsig.
1. The flow at a glance
POST /private/v1/users ─► create user, get UUID + sharedSecret
GET /private/v1/terms ─► fetch current totalVersion
POST /private/v1/users/USERID/terms ─► record acceptance
GET /private/v1/users/USERID/kyc-status ─► poll until SUCCESS
You can accept terms before KYC resolves; you can't trade until both are done. A wallet is automatically created when KYC reaches SUCCESS.
2. POST /private/v1/users
POST /private/v1/usersCreates a user and returns the things you'll need to operate on them later: a UUID, the initial KYC status (always PENDING), and the per-user shared secret.
Body (CreateUserRequest):
| Field | Required | Notes |
|---|---|---|
firstName | yes | Non-empty. Immutable. |
lastName | yes | Non-empty. Immutable. |
middleName | no | |
dateOfBirth | yes | YYYY-MM-DD. Age 19–125 is enforced. Immutable. |
ssnLastDigits | yes | Exactly 4 numeric digits. Immutable. |
addressLine1 | yes | |
addressLine2 | no | Apartment, suite, etc. |
city | yes | |
state | yes | 2-letter USPS state code. |
zip | yes | 5–10 characters. |
countryCode | yes | ISO 3166 alpha-2, e.g. US. |
phoneNumber | no | If provided, 10 numeric digits, unformatted. |
email | no | If provided, valid email format. |
emailVerifiedAt | yes | UTC timestamp. You must have verified the email before this call. |
phoneVerifiedAt | no | UTC timestamp. |
Response (201, CreateUserResponse):
{
"id": "00000000-0000-0000-0000-000000000000",
"kycStatus": "PENDING",
"sharedSecret": "<base64url-32-byte>"
}Save
sharedSecretimmediately and securely — it's not retrievable later. Store it encrypted, indexed by user UUID.
Failure modes:
409 user_already_exists— the identity hash (name + DOB + SSN) collides with another user under this ISV.422with aValidationErrorResponse— one or more fields failed validation.
3. Terms and conditions
The terms bundle is five documents (PRIVACY_POLICY, TERMS_OF_USE, MARKET_PARTICIPANT_AGREEMENT, RISK_DISCLOSURE_STATEMENT, RULEBOOK). Each has its own per-document version, and there's a bundle-level totalVersion that increments whenever any document version changes. The user must accept the current totalVersion.
GET /private/v1/terms — fetch the current bundle
GET /private/v1/terms — fetch the current bundleISV-only. No sub/subsig. Returns:
{
"totalVersion": 7,
"documents": [
{ "documentType": "PRIVACY_POLICY", "version": 3, "url": "https://..." },
{ "documentType": "TERMS_OF_USE", "version": 2, "url": "https://..." },
{ "documentType": "MARKET_PARTICIPANT_AGREEMENT", "version": 1, "url": "https://..." },
{ "documentType": "RISK_DISCLOSURE_STATEMENT", "version": 1, "url": "https://..." },
{ "documentType": "RULEBOOK", "version": 4, "url": "https://..." }
]
}Show the user each url, then capture their acceptance of totalVersion.
GET /private/v1/users/USERID/terms — has this user accepted?
GET /private/v1/users/USERID/terms — has this user accepted?User-scoped. Returns:
{ "accepted": true }POST /private/v1/users/USERID/terms — record acceptance
POST /private/v1/users/USERID/terms — record acceptanceUser-scoped. Body:
{ "totalVersion": 7 }Responses:
204— acceptance recorded.200— user had already accepted this version. Body is{ "accepted": true }.409— submitted version is stale. The body is aStaleTermsResponse:Re-fetch{ "error": "...", "code": "stale_terms", "currentTotalVersion": 8 }/terms, re-prompt the user with the newtotalVersion, and try again.
4. KYC
KYC runs asynchronously after POST /users — that call returns immediately with kycStatus: "PENDING". ProphetX runs the check against an identity-verification provider in the background.
GET /private/v1/users/USERID/kyc-status
GET /private/v1/users/USERID/kyc-statusUser-scoped. Returns:
{
"id": "<userId>",
"kycStatus": "SUCCESS",
"failReason": null,
"supportEmail": "[email protected]"
}Statuses:
PENDING— still in progress; poll again later.SUCCESS— user can trade. A wallet is automatically created at this point (for both SHARED and INDIVIDUAL ISVs).FAILURE—failReasonis populated with details. Direct the user tosupportEmailfor manual review.MORTALITY/PEP/OFAC— terminal; the user cannot trade.
A 5–10 second polling interval is plenty. KYC typically resolves in tens of seconds, so tight-looping just adds load without learning anything sooner.
5. Reading, updating, and deleting users
GET /private/v1/users/USERID
GET /private/v1/users/USERIDUser-scoped. Returns the verified user record — i.e. one that has cleared KYC.
Response (UserResponse):
{
"id": "00000000-0000-0000-0000-000000000000",
"firstName": "Ada",
"lastName": "Lovelace",
"middleName": null,
"dateOfBirth": "1985-12-10",
"addressLine1": "1 Main St",
"addressLine2": null,
"city": "Newark",
"state": "NJ",
"zip": "07102",
"countryCode": "US",
"phoneNumber": "1234567890",
"email": "[email protected]",
"emailVerifiedAt": "2026-05-12T12:00:00Z",
"phoneVerifiedAt": null,
"kycAt": "2026-05-12T12:00:38Z",
"createdAt": "2026-05-12T12:00:00Z",
"updatedAt": null
}Note that ssnLastDigits is not returned here.
Failure modes:
404— the user doesn't exist.409 user_pending_kyc— the user has been created but hasn't been promoted to a verified user yet. They may still bePENDING, or they may have terminally failed (FAILURE/MORTALITY/PEP/OFAC). CallGET /private/v1/users/USERID/kyc-statusto find out which.
PATCH /private/v1/users/USERID
PATCH /private/v1/users/USERIDUser-scoped. You can update any of these fields:
addressLine1,addressLine2,city,state,zip,countryCodephoneNumber,emailemailVerifiedAt,phoneVerifiedAt
Immutable fields (firstName, lastName, dateOfBirth, ssnLastDigits) cannot appear in the body. Including any of them returns 422 with a validation error.
DELETE /private/v1/users/USERID
DELETE /private/v1/users/USERIDUser-scoped. Returns 204 on success.
- INDIVIDUAL ISVs: returns
409 non_zero_balanceif the user's wallet isn't empty. Settle, refund, or withdraw first. - SHARED ISVs: deletion is allowed regardless of pot state, since the user row is a tally and the ISV pot is unaffected.
6. A curl walk-through
BASE="https://isv-staging-api.betprophet.co/private/v1"
# 1. Create user (ISV-only JWT)
curl -X POST "$BASE/users" \
-H "Authorization: Bearer $JWT_ISV" \
-H "Content-Type: application/json" \
-d '{
"firstName":"Ada","lastName":"Lovelace",
"dateOfBirth":"1985-12-10","ssnLastDigits":"1234",
"addressLine1":"1 Main St","city":"Newark","state":"NJ","zip":"07102",
"countryCode":"US","email":"[email protected]",
"emailVerifiedAt":"2026-05-12T12:00:00Z"
}'
# → 201 { "id":"<userId>", "kycStatus":"PENDING", "sharedSecret":"<save-me>" }
# 2. Get current terms, show them to the user
curl "$BASE/terms" -H "Authorization: Bearer $JWT_ISV"
# 3. Record acceptance (user-scoped — JWT has sub=userId + subsig)
curl -X POST "$BASE/users/<userId>/terms" \
-H "Authorization: Bearer $JWT_USER" \
-H "Content-Type: application/json" \
-d '{ "totalVersion": 7 }'
# → 204
# 4. Poll KYC
curl "$BASE/users/<userId>/kyc-status" -H "Authorization: Bearer $JWT_USER"
# → { "kycStatus": "SUCCESS", ... }
# 5. Read the verified user record (once KYC is SUCCESS)
curl "$BASE/users/<userId>" -H "Authorization: Bearer $JWT_USER"
# → 200 { "id":"<userId>", "firstName":"Ada", ..., "kycAt":"..." }Updated about 7 hours ago
