Onboard an investor through the API
Create a user through the API, resolve the participant's identity wallet, register the identity under a country, and verify the investor with Sumsub.
Overview
Onboarding an investor through the API is a sequence of calls: create the user, resolve the wallet that carries the identity, register the identity under a country, and verify the investor with a know-your-customer provider such as Sumsub. Each write needs a role, and a wallet protected by a code needs a verification proof on the same request. The wallet that carries the identity is not in the create response, and a user that exists with no identity contract takes one extra call before registration.
The sequence covers a participant created on a managed custody provider configuration, where the platform holds the key and signs on the participant's behalf. A bring-your-own-key configuration leaves the participant pending with a null custodyAccountId and a null identity until its owner proves the key at first sign-in, and that path is outside this guide. Custody Provider describes both kinds and how a configuration is chosen.
Creating a user deploys an identity contract for a managed participant, but it never registers it. Registration is a separate on-chain write, and a token reads the registration before it lets the wallet hold value, so a deployed but unregistered identity is not enough. An on-chain identityAn On-Chain Identity lets a subject's verification serve every asset that accepts it, so a screened wallet skips re-onboarding for the next regulated asset.Read the concept is the ledger record a regulated token checks, and a wallet without one is not addressable to any rule engine.
The sequence at a glance
The calls run in one order, and the wallet that carries the identity comes from the custody account rather than from the create response. A create that leaves the custody account settling is polled first, and a wallet with no identity contract takes the create path before registration.
You create the user with dalp.api.party.users.create. When that call settles with the custody account usable, you read the participant's custody accounts with dalp.api.participants.custodyAccountsList and take the identity subject wallet from the account. When the custody account is still settling, you poll the statusUrl the create response returned until the state is terminal, then read the accounts. With a wallet in hand, you register the identity with dalp.api.party.identities.register, and you verify the investor with Sumsub by requesting an SDK access token, then wait for the signed verdict. Only an approved verdict issues the claim the token requires; a rejected verdict revokes it, and an on-hold monitoring alert can revoke it when its severity reaches the topic's revocation threshold. A wallet that has no identity contract at all takes one extra call first, dalp.api.party.identities.create, and you wait for the identity read to answer before registering.
How the systems call each other
One request from you starts work in several systems. The figure shows who calls whom for a managed Fireblocks configuration and a Sumsub verification provider. Each numbered arrow from your backend is one of the steps below.
The numbers match the steps below. The investor talks to Sumsub directly, and a returned token is not a verification: a signed Sumsub verdict or on-hold alert determines the claim change.
Registration in step 3 is a write by the identity manager that calls it, so the platform signs it with that caller's own key, not with the investor's Fireblocks configuration. It reaches Fireblocks only when the caller's key is held in a Fireblocks configuration as well. In the default provider-native mode the platform files each transaction with Fireblocks and Fireblocks broadcasts it.
Step 1: create the user
You call POST /api/v2/users with the user's email, an optional name, and the providerConfigurationId of an enabled managed provider configuration in the active organization. Naming the configuration is what creates the participant, mints the key, and deploys the identity contract. Omit it and the user holds no participant, so the rest of this sequence has no wallet to carry. The call needs the identity-manager role.
The create response names no wallet address. Read custodyAccountId and identity from a settled response, and resolve the wallet in step 2.
curl -X POST "https://your-instance.example.com/api/v2/users" \
-H "X-Api-Key: sm_dalp_..." \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"name": "Amara Okafor",
"providerConfigurationId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d10",
"walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }
}'A settled call answers 200 with the user and its participant:
{
"data": {
"id": "0193fa5c-9c31-7a4d-b0e2-3f6c9d1a7b45",
"name": "Amara Okafor",
"email": "[email protected]",
"participantId": "pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12",
"activationStatus": "active",
"custodyAccountId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d11",
"identity": "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
},
"links": { "self": "/v2/users/0193fa5c-9c31-7a4d-b0e2-3f6c9d1a7b45" }
}A 200 means the custody account settled and is usable. A 202 means the user exists but the custody account is still settling, and the body carries the lifecycle handle instead:
{
"requestId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b",
"state": "awaiting-aa-reconciliation",
"statusUrl": "/api/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-account-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b"
}Poll the statusUrl the response returned, and follow it rather than building a URL of your own. The lifecycle request belongs to the participant the call created, so the read is participant-scoped at /api/v2/participants/:participantId/custody-account-requests/:requestId; a /users/me path does not reach it, because the caller is not that participant. Poll until the state is succeeded, failed, or expired. A terminal failed or expired state means the custody account is unusable even though the user row exists, and you must resolve that before continuing.
Both responses accept Idempotency-Key. The identity writes in steps 3 and 5 treat a repeated key as a retry of the original submission. Idempotency and async settlement covers the key's lifetime and the Prefer header that decides whether a write settles inline.
Step 2: resolve the identity wallet
You call GET /api/v2/participants/:participantId/custody-accounts with the participantId from step 1. Each item carries its ledger and network, its lifecycle, its signingAddress, and an optional smartWalletAddress.
Take the identity subject wallet as the smart wallet address when the account has one, and otherwise as the signing address of the account marked primary. That is the address the Console carries through its own identity actions, and it is the address every following call in this sequence must use.
curl "https://your-instance.example.com/api/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-accounts" \
-H "X-Api-Key: sm_dalp_..."{
"data": [
{
"custodyAccountId": "0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d11",
"participantId": "pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12",
"ledger": "evm",
"network": "11155111",
"accountLifecycle": "active",
"contextLifecycle": "active",
"isPrimary": true,
"signingAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"smartWalletAddress": null
}
],
"links": { "self": "/v2/participants/pp_0198f2f0-6d0b-7f86-8f24-c5e1ef9f9d12/custody-accounts" }
}Read signingAddress and smartWalletAddress only from an account whose accountLifecycle and contextLifecycle are active. A participant can hold accounts on more than one network, so select the account on the network you are registering on rather than the first row. When the accounts on that network do not agree on one smart wallet address, or none of them is marked primary, the subject wallet is ambiguous; resolve the account selection before you continue, because the platform does not choose one for you.
Step 3: register the identity
You call POST /api/v2/system/identity-registrations with the country to record and the wallet from step 2. The country is an ISO 3166-1 alpha-2 code such as BE, and the platform normalizes it before the write. Send wallet explicitly: omitting it registers the effective wallet of the caller, which is the operator's wallet and not the investor's.
The call needs the identity-manager role. It also accepts walletVerification, which proves the caller's own wallet rather than the wallet being registered.
curl -X POST "https://your-instance.example.com/api/v2/system/identity-registrations" \
-H "X-Api-Key: sm_dalp_..." \
-H "Idempotency-Key: register-identity-amara-001" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"country": "BE",
"walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }
}'A settled call answers 200 with the identity, the transaction hashes, and the transaction request behind them:
{
"data": {
"id": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"account": { "id": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "contractName": null },
"isContract": false,
"hasIdentity": true,
"registered": { "isRegistered": true, "country": "BE" },
"claims": []
},
"meta": {
"txHashes": ["0x5c73a1e94b0d268fa37e15c8b02d94f6a1e7835c0b96d2f4e83a17c5d602b9f4"],
"transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b"
},
"links": { "self": "/v2/system/identity-registrations" }
}registered appears only once the registry entry is active. The route also answers 202 with a queue handle when the write is queued rather than settled:
{
"transactionId": "0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b",
"status": "QUEUED",
"statusUrl": "/api/v2/transaction-requests/0198f1a2-3c4d-7e8f-9a0b-1c2d3e4f5a6b"
}Poll that statusUrl until the request reports a terminal state. The call registers the wallet together with its paired address on that network, the smart wallet for a signing address or the signing address for a smart wallet, and submits only the addresses whose registration is not yet active. A repeated registration is safe rather than conflicting: when every one of those addresses is already active, the route answers with the identity and submits no transaction.
Until an identity contract exists for the wallet, the route answers 404 with DALP-9513, whose fix names POST /api/v2/identities for that wallet. Take the recovery path below rather than retrying the same registration.
Step 4: verify the investor with Sumsub
Registration makes the wallet addressable to every token's rule check. It proves nothing about what the participant may hold, so a registered identity that carries no required claim is still refused by a token that requires one. This step collects that claim through Sumsub. Sumsub runs the checks its configured level defines, such as documents and liveness. DALP turns the result into an on-chain claim.
You need a Sumsub compliance provider with an active claim topic, such as knowYourCustomer, before you start. An administrator adds it once in the Console; Compliance providers covers the setup. Your integrator backend passes the configured providerId to the investor app. A backend that looks it up with GET /api/v2/compliance/providers needs an Admin, System manager, or Compliance manager role. The investor's own session does not need those roles to request an SDK token. The call below refuses an identity that has no registration in the system's identity registry, so run it after step 3.
Issue an SDK access token
You call POST /api/v2/compliance/subjects/sdk-access-token to let the investor's app run the Sumsub check without holding Sumsub credentials. Send the providerId and topicName. Omit identityAddress to use the caller's own registered identity. A backend that acts for the investor names the investor's identityAddress. The platform creates the Sumsub applicant first when the identity has none, and binds it to that identity.
curl -X POST "https://your-instance.example.com/api/v2/compliance/subjects/sdk-access-token" \
-H "X-Api-Key: sm_dalp_..." \
-H "Content-Type: application/json" \
-d '{
"providerId": "b6f2c0a4-1d83-4f1e-9c47-2a5e8d0b7311",
"topicName": "knowYourCustomer",
"identityAddress": "0x8ba1f109551bD432803012645Ac136ddd64DBA72"
}'{
"data": { "token": "_act-sbx-example-access-token", "expiresAt": "2026-09-28T16:10:00.000Z", "levelName": "id-and-liveness" },
"links": { "self": "/api/v2/compliance/subjects/sdk-access-token" }
}Pass token to the Sumsub WebSDK or MobileSDK in the investor's app. The token is short-lived, so request a new one after expiresAt instead of storing it. Naming an identity other than the caller's own needs a compliance provider role, and a missing role answers 404 with DALP-12283, the scope-denied error, not 403. The same code covers the other scope failures listed under step 4 failures.
Handle verdicts and on-hold alerts
Sumsub reviews the submission and sends a signed webhook to the platform. The platform checks the signature against the webhook secret of the provider. You enter that secret on the provider in the Console. The Console then shows a delivery URL for each topic, and you copy that URL into the Sumsub dashboard so Sumsub sends its webhooks there. Compliance providers covers both. For the provider's claim topic, such as knowYourCustomer, the platform handles Sumsub events as follows:
| Sumsub event | Claim on the identity |
|---|---|
applicantReviewed with GREEN (approved) | The platform issues the claim from the provider's issuer identity |
applicantReviewed with RED (rejected) | The platform revokes the claim |
applicantOnHold | The platform records a monitoring alert and revokes the claim when its severity is at or above the topic's revocation threshold; below that threshold, no on-chain change |
An on-hold alert has severity 50 for reviewRejectType: RETRY, 100 for FINAL, and 70 otherwise. Sumsub emits only approved and rejected verdicts through this integration; an on-hold alert is not an under_review verdict. Subscribe to the compliance.verification-verdict.final event for approved and rejected verdicts. On-hold alerts do not emit this event. Confirm the current claim on the identity before a transfer. A token whose compliance rules require the topic reads the claim when a transfer runs; an earlier approval does not guarantee the claim is still present.
Step 4 failures
| Symptom | Cause | What to do |
|---|---|---|
404 DALP-12283 | The call named another identity without a compliance provider role | Retry with the role, or omit identityAddress for the caller's own identity |
404 DALP-12283 | The call omitted identityAddress, and the caller holds no active identity in the system's identity registry | Register the caller's identity first, or name the identity the session is for |
404 DALP-12283 | The call omitted identityAddress, and the caller holds more than one active identity | Name the identity in identityAddress |
404 DALP-12283 | A caller with the role named an identity that has no registration in the system's identity registry | Register that identity in step 3, then request the token again |
| The SDK reports an expired token | The access token passed expiresAt | Request a new token |
| No event after the investor submits | Sumsub is not sending to the topic's delivery URL, or the signing secret differs between Sumsub and the provider | Copy the delivery URL from the provider's "Webhooks" table into the Sumsub dashboard, and check that both sides use the same secret |
Verify a participant covers the remaining work in the Console: confirm the claim on the identity, add an issuer-signed verification by hand when a topic has no provider, and run the token's pre-check before you submit a transfer.
Step 5: recover a wallet with no identity contract
One state leads here: a later GET /api/v2/system/wallets/:wallet/identity for the step 2 wallet answers 404 with DALP-9512. That says the wallet has no identity contract. The user create deploys the identity before it answers, so this state follows a create that answered DALP-12107: the user was kept but the outcome of its identity deployment is unknown, and this call completes it against the chain. A create whose deployment the chain failed answers DALP-12115 and keeps no user, so repeat that create with a new Idempotency-Key instead. A create response whose identity is null is a different case: the user holds no custody key at all (no provider configuration was named, or the configuration is bring-your-own-key), so step 2 has no wallet to resolve and this recovery does not apply.
You call POST /api/v2/identities with the step 2 wallet and walletVerification, then run step 3 once the identity read answers.
curl -X POST "https://your-instance.example.com/api/v2/identities" \
-H "X-Api-Key: sm_dalp_..." \
-H "Idempotency-Key: create-identity-amara-001" \
-H "Content-Type: application/json" \
-d '{
"wallet": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "482913" }
}'A caller that does not own the wallet needs the identity-manager role for this call. An operator onboarding another user does not own that user's wallet, so it needs the role, and a refusal answers DALP-9439.
Wait for the identity read to answer before you register. Reads serve indexed state, so a contract that has just been deployed can still read as missing for a short window, and a registration sent inside that window answers DALP-9513 again. GET /api/v2/system/identity-registration-statuses reports one of NO_SYSTEM, NO_IDENTITY, NOT_REGISTERED, PENDING, or ACTIVE for one wallet, which is one read to branch on instead of combining several.
A create for a wallet that already has an identity contract is refused with DALP-0024. Read the identity's current state rather than repeating the call.
Recover a user whose create failed
A create that failed outright and now answers DALP-0670 on retry is a duplicate email: the organization already holds a user with that address. List the organization's users, locate the record carrying that email, and read its participantId. Then run this same recovery from step 2 with that participant's custody account. The email is unique per organization, so the retry cannot create a second user.
Roles and wallet verification
Every write in the sequence is a mutation, and each one checks the caller before it queues work.
| Call | Role the caller needs | Wallet verification |
|---|---|---|
POST /api/v2/users | Identity manager | When the calling wallet is protected by a code |
GET /api/v2/participants/:participantId/custody-accounts | Access follows participant access in the active organization | Not applicable; a read submits no transaction |
POST /api/v2/system/identity-registrations | Identity manager | When the calling wallet is protected by a code |
POST /api/v2/compliance/subjects/sdk-access-token | The session alone for the caller's own identity; a compliance provider role for another identity | Not applicable; the call submits no transaction |
POST /api/v2/identities | Identity manager when the caller does not own the wallet; the session alone when it does | When the calling wallet is protected by a code |
The SDK-token route is the exception: it answers 404 with DALP-12283 for a caller who names another identity without the role. Every other missing role answers 403 with DALP-9439. walletVerification carries the step-up proof for the caller's own wallet: the method and its secret, such as a pin code, a one-time password, a recovery code, or an identity-provider code.
Errors
| Code | Status | When it appears | What to do |
|---|---|---|---|
| DALP-0670 | 409 | The email is already a user in the organization | Find the existing user by email and run the recovery in step 5 |
| DALP-12115 | 409 | The chain failed the created user's identity deployment, so no user was kept | Repeat the create with a new Idempotency-Key |
| DALP-12107 | 503 | The user was created but the outcome of its identity deployment is unknown | Run the recovery in step 5 for the user's wallet |
| DALP-12103 | 503 | The user create has not settled inside the synchronous window | Repeat the request with the same Idempotency-Key to wait for it |
| DALP-9524 | 404 | The organization has no deployed system on the chain yet | Deploy the system, then repeat the request |
| DALP-9512 | 404 | The identity read finds no identity for the requested reference | Treat the wallet as having no identity contract and run step 5 |
| DALP-9513 | 404 | Registration for a wallet with no identity contract | Create the identity first with POST /api/v2/identities, wait for it to index, then register again |
| DALP-0024 | 409 | An identity contract already exists, or another write on the wallet is in flight | Read the identity's current state before sending the operation again |
| DALP-9439 | 403 | The caller's executor holds none of the roles the route requires | Retry as an identity manager, or grant the role before retrying |
| DALP-12033 | 400 | A signing request carried no walletVerification block | Add walletVerification with an enrolled method and repeat the request |
| DALP-12034 | 401 | The request named a verification method the account has never enrolled | Enroll the method in wallet security settings, or send one the account already holds |
| DALP-12035 | 403 | The verification credential does not match, or it was already used | Send a current credential, and use a fresh recovery code because a recovery code works once |
| DALP-12036 | 429 | Repeated verification failures locked verification for a cooldown | Wait the seconds in data.retryAfterSeconds, then verify again |
DALP error index lists every code with its machine name, category, and retry flag. Read and handle DALP errors covers the envelope these codes arrive in.
Next steps
- Idempotency and async settlement covers the
Idempotency-KeyandPreferheaders on these writes, and the status URL a queued write returns. - Verify a participant completes the eligibility work a registered identity still needs.
- Custody Provider explains what a managed configuration holds, and what a bring-your-own-key participant still has to prove.
Idempotency and async settlement
Learn where Idempotency-Key deduplicates queued transactions and use Prefer to wait for on-chain settlement or return immediately.
Sale terms and acknowledgement
Upload a Primary Offering's terms documents, record their hash once during setup, then list, download, acknowledge and buy as an investor.