API reference: Managed Custody SDK KYC
Onboard users with KYC completed on the user's device via the UR-issued Sumsub access token.
This page documents the alternative onboarding path for Managed Custody Mode where the user completes KYC on their own device using UR's Sumsub tenant. Your backend exchanges the user's URID for a short-lived Sumsub access token, and the Sumsub mobile or web SDK drives the KYC workflow directly with the user.
Use this path when you do not run a Sumsub tenant of your own and you want UR to host the KYC vendor relationship end to end. For the alternative path where your platform completes KYC in your own Sumsub tenant and shares the applicant with UR through Sumsub reuse, see API reference: Managed Custody Mode.
This onboarding path requires your partnerId to be configured for SDK mode in UR Nacos. Coordinate with your dedicated integration channel before pointing production traffic at it.
1. Where this fits
Managed Custody Mode supports two KYC onboarding paths. Both produce the same end state (the user's URID is minted, the UR-managed wallet is provisioned, and the bank account is activated). They differ only in who runs the Sumsub workflow.
Sumsub tenant
Your platform
UR
Who runs KYC UX
Your platform
Sumsub SDK in your app, on the user device
KYC data lands at UR via
Sumsub share token + /kyc/check polling
Sumsub webhooks (server to server)
Partner backend calls
/create-account (with applicantId), /kyc/check, /kyc/form-a-info, /kyc/sign-form, /kyc/submit
/create-account (no applicantId), /kyc/sumsub-access-token, /kyc/session/current, /kyc/form-a-info, /kyc/sign-form, /kyc/submit
Onboarding state shape
Coarse (PartnerDataIngestion, IdentityVerification, SignFormA, Register)
Fine-grained (eight steps; first is ConfirmYourCountryOfResidence, last before submit is SignFormA)
Retry on KYC rejection
Your platform restarts in your own Sumsub
Restart by calling /create-account again. Identity reuse is supported.
The webhook contract (fma.account.result) and all post-onboarding banking APIs are identical to the Sumsub reuse path. Once the user reaches the Live state, your integration uses the same Managed Custody Mode endpoints regardless of which KYC path got them there.
2. End-to-end sequence
3. Integration walkthrough
Create the account
Call POST /api/fma/v1/create-account with the partner-side X-External-User-Id header and the user's onboarding data in the body. UR mints the URID, provisions the UR-managed wallet, and creates an onboarding session bound to sessionId.
Unlike the Sumsub reuse path, you do not send applicantId. UR's Sumsub tenant will assign the applicant id later, once the user actually starts the SDK.
Persist sessionId, urId, and evmAddress before showing onboarding progress to the user.
Request a Sumsub access token
Call POST /api/fma/v1/kyc/sumsub-access-token with an empty body. The endpoint sits under the same FMAValidate middleware as the rest of /api/fma/v1/kyc/*; UR reads urId from the X-Ur-Id header you already sign on each request, resolves the active session server-side, and returns a Sumsub access token with a fixed 20 minute TTL.
If the user pauses before launching the SDK, request a new token; UR re-issues a fresh token bound to the same Sumsub applicant so the workflow resumes where the user left off.
Launch the Sumsub SDK
Pass the token to your partner app and initialize the Sumsub mobile or web SDK. The user completes Country, Address, ID scan, and Liveness levels on their device. Your backend does not need to call UR during this phase; UR receives Sumsub webhooks server to server.
If the user navigates away mid-flow, refresh the token by calling /api/fma/v1/kyc/sumsub-access-token again and relaunch the SDK with the new token.
Wait for KYC completion
Detect SDK completion using one of two patterns:
Push from your partner app to your backend when the SDK reports done.
Poll
GET /api/fma/v1/kyc/session/currentfrom your backend every one to five seconds for up to one minute after the SDK starts.
The session reaches state SignFormA once Sumsub delivers a GREEN applicantWorkflowCompleted webhook and UR has stored the verified KYC snapshot. Sumsub webhook delivery is typically subsecond but the 99th percentile can reach 30 seconds; treat the 60 second polling window as the practical timeout.
If Sumsub returns RED, UR fires fma.account.result with status set to rejected and rejectCode set to SUMSUB_REJECTED. You can offer the user a retry path by starting again at POST /api/fma/v1/create-account with the same X-External-User-Id.
Render Form A and capture consent
Call GET /api/fma/v1/kyc/form-a-info?sessionId=... and display data.text to the user exactly as returned. The text includes the user's verified KYC identity and the banking terms; obtaining explicit user consent at this step is a regulatory requirement.
Store data.textHash for the next step. UR signs the same text on its side under a session lock to prevent any divergence between what the user saw and what UR signs.
Sign Form A
After the user consents, call POST /api/fma/v1/kyc/sign-form with sessionId and the textHash from the previous step. UR re-renders Form A under a session lock and rejects the call with FORMA_TEXT_MISMATCH if the hash no longer matches; that signals the staging data changed underneath you. Recover by re-calling form-a-info and resigning with the new hash.
UR's TurnKey wallet signs the Form A text on the user's behalf. The returned signerAddress equals the evmAddress from the create-account response.
Submit the session
Call POST /api/fma/v1/kyc/submit with sessionId. UR returns state: Submitting with queued: false. The session is now owned by UR's background scheduler.
queued: false is the success signal for this onboarding path; it indicates that UR can proceed with bank activation immediately without waiting for any further user action. The legacy queued: true response shape is reserved for an unrelated penny-transfer path that the SDK flow does not use.
Receive the activation webhook
UR runs the bank activation asynchronously (typical latency 30 seconds to five minutes on testnet, 10 to 60 seconds on mainnet). When the URID reaches Live status on chain and the bank account is open, UR delivers fma.account.result with status: activated to your registered webhook URL.
Your webhook handler must verify the EIP-191 signature (X-Api-Signature / X-Api-Signature-V2), deduplicate on the X-Webhook-Request-Id header, and ACK with HTTP 200. UR retries non-2xx responses with exponential backoff.
4. API reference
All endpoints below sit under Partner Auth. See Signature and verify for the EIP-191 signing scheme and the canonical-message construction rules.
Production
https://openapi.ur.app
Preview
https://openapi-preview.ur.app
Testnet
https://uropenapi-qa.ur-inc.xyz
Confirm the exact base URL set with UR before production rollout.
The examples in this section use the testnet base URL.
4.1 Create account
Create the user's URID and the onboarding session. UR provisions a UR-managed wallet and stores the session in state ConfirmYourCountryOfResidence.
Method
POST
Path
/api/fma/v1/create-account
Headers
Partner Auth headers + X-External-User-Id (no X-Ur-Id yet)
Request body:
Request fields:
email
Yes
The user's email address. Must be unique across UR partners; reusing the same email under a different partner returns L1_CONFLICT.
nationality
No
ISO 3166-1 alpha-3 country code. Sending it lets UR fail country gates before the URID is minted.
residency
No
ISO 3166-1 alpha-3 country code of residence.
dob
No
Date of birth in YYYY-MM-DD format.
documentExpiry
No
Passport or ID expiry date in YYYY-MM-DD format.
Response:
Response fields:
sessionId
UUID for this onboarding attempt. Constant for the lifetime of the attempt. Use it in every subsequent KYC endpoint on this page.
urId
The user's URID, equal to the on-chain NFT token id. Stable for the user's lifetime across all retries.
evmAddress
The user's UR-managed wallet address. Form A is signed by this address.
state
Current session state. ConfirmYourCountryOfResidence is the entry state for this onboarding path.
idempotentReplay
true when an active onboarding session already exists for (partnerId, X-External-User-Id). The response returns the existing session unchanged so repeated calls cannot mint duplicate URIDs.
Rules:
Repeated calls with the same
X-External-User-Idwhile a session is active return the samesessionIdand setidempotentReplaytotrue. The user's URID and wallet address never change across these retries.After the active session terminates (success, rejection, or timeout), the next call mints a fresh session with an incremented
retryLevel. The URID is reused; only the onboarding session is new.
4.2 Create Sumsub access token
Issue a short-lived Sumsub access token for the user. The token authenticates the Sumsub SDK against UR's Sumsub tenant.
Method
POST
Path
/api/fma/v1/kyc/sumsub-access-token
Headers
Partner Auth headers + X-Ur-Id
Request body: empty ({}).
UR reads urId from the X-Ur-Id header (which you already sign as part of Partner Auth on every /api/fma/v1/* request) and resolves the active onboarding session server-side. No tokenId or network fields are needed.
Response:
Response fields:
token
Sumsub access token. TTL is 20 minutes. Pass this string to the Sumsub SDK constructor on the user device.
Server-side validation rules:
X-Ur-Idmust be present (elseINVALID_PARAM).The user must have an active onboarding session for
(partnerId, urId)(elseNO_ACTIVE_SESSION).The session's data channel must be the SDK path (else
SESSION_WRONG_CHANNEL; check your partner config).
4.3 Get current session
Read the current onboarding state. Useful for polling after the user starts the Sumsub SDK.
Method
GET
Path
/api/fma/v1/kyc/session/current
Headers
Partner Auth headers + X-Ur-Id
Request: no query parameters or body.
Response with an active session:
Response fields:
state
Current session state. See the state enum table below.
dataChannel
Always sdk for this onboarding path. If you see anything else, your partner config is wrong.
retryLevel
Increments each time the user starts a fresh onboarding attempt after a previous one terminated.
createdAt, lastUserActivityAt
Unix timestamps in seconds.
A user with no active session receives NO_ACTIVE_SESSION (code: 30031); treat that as the post-onboarding steady state.
State enum
state reflects the step the session is currently parked on, in onboarding order:
state
Phase
Meaning
ConfirmYourCountryOfResidence
Sumsub SDK
Initial state right after /create-account. The user is at the start of the Sumsub questionnaire, confirming their country of residence.
AddressProof
Sumsub SDK
The user is confirming and proving their residential address.
UnderstandRisk
Sumsub SDK
The user is completing the risk-understanding questionnaire.
LiabilityWaiver
Sumsub SDK
The user is acknowledging the liability waiver.
IdOrPassportOrOtherIdInformationScan
Sumsub SDK
The user is scanning their ID card or passport.
IdAndLiveness
Sumsub SDK
The user is completing the liveness check and face match.
SignFormA
Partner backend
The Sumsub phase finished with GREEN. Your backend should now call /kyc/form-a-info, collect the user's consent, /kyc/sign-form, then /kyc/submit. The state stays SignFormA through sign-form; only /kyc/submit advances it.
Register
UR backend
/kyc/submit succeeded and UR's background activation scheduler owns the session. Nothing for you to do — wait for the fma.account.result webhook.
Notes on reading state:
Progress through the Sumsub-phase states is driven by Sumsub's webhooks to UR, so this endpoint can lag the SDK UI by a few seconds, and a poll loop will not necessarily observe every intermediate state.
Submitting,Completed, andFailednever appear on this endpoint./kyc/submitreturnsSubmittingin its own response, but/kyc/session/currentkeeps reportingRegisterwhile activation is in flight. Once the session reaches a terminal outcome (activated or rejected), this endpoint returnsNO_ACTIVE_SESSION(code: 30031) — the outcome itself is delivered by thefma.account.resultwebhook (section 5).Treat any unlisted value as an in-progress state: keep the user in the Sumsub SDK and keep polling.
4.4 Get Form A
Render Form A from the verified KYC snapshot.
Method
GET
Path
/api/fma/v1/kyc/form-a-info
Headers
Partner Auth headers + X-Ur-Id
Query
sessionId
Callable only while state is SignFormA.
Response:
Response fields:
formAVersion
Current Form A template version. Pin this in your audit trail.
text
The Markdown text the user must consent to. Render exactly as returned.
textHash
Keccak-256 hash of the text bytes. Pass it back unchanged in /kyc/sign-form so UR can detect any tampering between render and sign.
4.5 Sign Form A
UR's TurnKey wallet signs Form A on the user's behalf. The user authorized the text by consenting in your UI; you prove that authorization by echoing the textHash.
Method
POST
Path
/api/fma/v1/kyc/sign-form
Headers
Partner Auth headers + X-Ur-Id
Request body:
Response:
The signerAddress matches the evmAddress from /create-account. The returned state is still SignFormA; the next call (/kyc/submit) flips the session to Submitting.
Common errors:
20005
FORMA_TEXT_MISMATCH
The stored text changed between /form-a-info and /sign-form.
Re-call /form-a-info, show the new text, then sign with the new hash.
20007
FORMA_INCOMPLETE
The session is missing required fields.
Should not happen after a Sumsub GREEN; contact your integration channel.
40005
TURNKEY_SIGN_FAILED
UR's signing subsystem hit a transient failure.
Retry after a short backoff.
4.6 Submit
Hand the session off to UR's background activation scheduler.
Method
POST
Path
/api/fma/v1/kyc/submit
Headers
Partner Auth headers + X-Ur-Id
Request body:
Response:
queued: false is the success signal for this onboarding path; UR's background scheduler picks the session up immediately. Wait for the fma.account.result webhook to confirm activated.
5. Webhook contract
UR delivers a single event type during onboarding: fma.account.result. Subscribe at your registered webhook URL and verify the request signature before processing. Webhooks are EIP-191 signed (not HMAC): X-Api-Signature carries V1 = sign(body) and X-Api-Signature-V2 carries V2 = sign(timestamp + "." + requestId + "." + body). See Webhooks and Signature and verification for the exact recipe.
5.1 Envelope
The body envelope is { event, data, timestamp }. Delivery metadata (including the idempotency id) travels in X-Webhook-* headers, not in the body.
Dedupe on the X-Webhook-Request-Id header — it is fixed for the lifetime of a message and repeats on every retry. UR retries non-2xx responses with the same request id (exponential backoff); your handler must be idempotent on that value. (businessKey is an internal UR dedup key and is not part of the wire envelope.)
5.2 Activated payload
5.3 Rejected payload
The most common rejectCode values for this onboarding path:
SUMSUB_REJECTED
Sumsub
Vendor-side terminal rejection.
Allow retry. Suggest clearer document images.
KYC_REJECTED
Banking partner
Downstream compliance rejected the user after Sumsub passed.
Surface a generic non-onboarded message; do not encourage immediate retry.
SESSION_EXPIRED
UR maintenance
The user abandoned mid-flow for more than seven days.
Allow restart.
NATIONALITY_RESTRICTED, RESIDENCY_UNSUPPORTED
UR gates
Country gates failed late.
Should normally surface at /create-account; if it arrives via webhook, contact your integration channel.
6. Failure handling
6.1 Sumsub rejection
When Sumsub returns RED, UR closes the onboarding session and dispatches fma.account.result with status: rejected, rejectCode: SUMSUB_REJECTED. Your platform can offer a retry by calling /create-account again with the same X-External-User-Id; UR mints a fresh sessionId and increments the internal retryLevel. The user's URID is preserved.
The retry is safe to attempt immediately. UR's onboarding pipeline keeps an identity reservation so the same Sumsub-verified person cannot have two active onboarding attempts at once; that reservation is released automatically when the current attempt terminates, including on Sumsub rejection.
6.2 User abandons mid-flow
If the user starts onboarding but never finishes, UR's background sweep marks the session expired after seven days of inactivity and dispatches fma.account.result with status: rejected, rejectCode: SESSION_EXPIRED. Your platform can offer a restart any time; calling /create-account after the expiration mints a fresh session.
While the seven-day window is open, /kyc/session/current continues to return the in-progress state. Your UI can resume the user from whichever step the session is in.
6.3 Identity reuse
UR enforces an anti-fraud check so that the same identity (matching on name plus date of birth) cannot run two simultaneous onboarding attempts across any partner. If the second attempt collides with an already-active first attempt, UR rejects the collision. Sequential retries by the same identity are always allowed; the previous attempt's reservation is released when it terminates.
You do not need to implement client-side dedup. UR handles the check; partners only see a rejection if the collision is intentional fraud.
6.4 Token refresh
Sumsub access tokens expire 20 minutes after issuance. If the user pauses partway through the SDK, your backend re-issues a token via POST /api/fma/v1/kyc/sumsub-access-token; the Sumsub SDK exposes a callback (typically called getNewAccessToken) for in-session refresh. UR re-binds the new token to the same Sumsub applicant, so the SDK resumes where the user left off.
7. Reference: integration test
The Go integration tests below ship in the UR backend repository and run against the testnet environment. Use them as a port reference for your language. Full source: tools/callurbankapi/fma_sdk_test.go and tools/callurbankapi/fma_kyc_onboarding_test.go.
7.1 Phase 1 and 2: account plus Sumsub token
7.2 Phase 3: sign and submit
7.3 Partner Auth signing helper
8. Error codes
The table below covers the codes that surface during this onboarding path. Endpoint-specific error tables for the rest of the Managed Custody Mode surface live next to each endpoint in API reference: Managed Custody Mode.
10002
PARTNER_NOT_ALLOWED
/create-account
partnerId is not on UR's allowlist; contact your integration channel.
10004
PARTNER_MODE_MISMATCH
/create-account
The partner is not configured for the SDK onboarding path.
30010 to 30013
country gates
/create-account
Sent nationality, residency, dob, or documentExpiry fails a country or age gate.
30015
L1_CONFLICT
/create-account
The email is already onboarded by another partner.
30031
NO_ACTIVE_SESSION
/api/fma/v1/kyc/sumsub-access-token, /kyc/session/current
The user has no active onboarding session. Call /create-account first.
30032
SESSION_WRONG_CHANNEL
/api/fma/v1/kyc/sumsub-access-token
The active session is on a different onboarding path; partner config issue.
20005
FORMA_TEXT_MISMATCH
/kyc/sign-form
textHash does not match the current render; re-call /kyc/form-a-info.
40005
TURNKEY_SIGN_FAILED
/kyc/sign-form
UR's signing subsystem hit a transient failure; retry.
40003
UPSTREAM_UNAVAILABLE
any
UR's upstream (banking partner, TurnKey, Sumsub) is temporarily unavailable. Retry with backoff.
50002
INTERNAL_ERROR
any
Surface a generic message and contact your integration channel.
Last updated