← Documentation

Quickstart

Sandbox-first path: mint a bi_test_ key, verify a person, create and bind an account.

Take one user from unverified to a fully owned blockchain account. Every call below is curl — no SDK required.

Start on the sandbox. Production runs live identity checks and live chain writes. The sandbox uses free test verifications against the real provider and a simulated chain.

Sandbox vs production

SandboxProduction
Base URLhttps://sandbox.identityinc.iohttps://api.identityinc.io
API key prefixbi_test_…bi_…
How to get a keyPOST /v1/billing/checkout on the sandbox URL (below)Signup on the marketing site
ChainSimulated (chain:mock)Live Vaulta/EOS (chain:live)
Identity checksReal Shufti flow, test account — freeReal Shufti, production account — billed by provider
Provider callback hostsandbox.identityinc.ioproduction API host

Confirm which environment you hit:

curl -s https://sandbox.identityinc.io/v1/health
# {"ok":true,...,"mode":{"chain":"mock","kyc":"shuftipro"}}

curl -s https://api.identityinc.io/v1/health
# {"ok":true,...,"mode":{"chain":"live","kyc":"shuftipro"}}

Never send a bi_test_ key to production, or a bi_ key to the sandbox — they are different deployments with different databases.

0. Get a sandbox API key

The marketing checkout at identityinc.io provisions production keys. For sandbox, call checkout on the sandbox host:

curl -X POST https://sandbox.identityinc.io/v1/billing/checkout \
  -H "Content-Type: application/json" \
  -d '{
        "planId": "builder",
        "email": "you@example.com",
        "orgName": "My App",
        "tenantSlug": "myapp-dev",
        "acceptTerms": true
      }'

The response includes apiKey once (prefix bi_test_). Store it server-side.

export BI_URL=https://sandbox.identityinc.io
export BI_KEY=bi_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

When you are ready for live traffic, sign up at checkout, switch BI_URL to https://api.identityinc.io, and use the bi_… key from signup.

The model in one minute

Three objects, and the whole API is about the relationship between them.

PersonA verified human. did:web:…. Created by KYC, never by you directly.
AccountA blockchain account. did:antelope:…. Starts provisional until unlock.
BindingThe cryptographic link between the two, signed by the user's own key.

The flow is always the same shape: verify the person → create the account → bind them → unlock. An account is only fully the user's after unlock.

DID prefixes are validated, so passing an accountDid where a personDid belongs fails with VALIDATION_ERROR rather than doing something surprising.

1. Verify the person

Start KYC with no account — this is person-first, and it is what the default organization policy requires before any free account is created.

curl -X POST "$BI_URL/v1/kyc/start" \
  -H "Authorization: Bearer $BI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
{
  "personDid": "did:web:…:people:550e8400-e29b-41d4-a716-446655440000",
  "provider": "shuftipro",
  "applicantId": "...",
  "accessToken": "...",
  "verificationUrl": "https://app.shuftipro.com/...",
  "levelName": "basic"
}

Send the user to verificationUrl, or use accessToken to mount the provider's onsite SDK in your own UI. Identity documents go to the provider, not to us — we hold hashes and the decision.

On the sandbox, complete the flow with a Shufti test ID — not a real passport. Test documents are free on the sandbox's Shufti test account. On production, every start is a paid verification; always send an Idempotency-Key so a network retry does not bill you twice. See Idempotency.

No OTP in sandbox KYC. Verification is document + face with a test ID — BoundIdentity does not enable Shufti phone OTP for KYC. Phone/email OTP endpoints are optional contact proof (off on sandbox); you do not need them for the path below.

Keep personDid. It is how you refer to this human from now on.

2. Wait for approval

Register a webhook for kyc.approved — that is the intended path, and it avoids a polling loop entirely.

If you must poll:

curl -s "$BI_URL/v1/kyc/status?personDid=$PERSON_DID" \
  -H "Authorization: Bearer $BI_KEY"
{
  "personDid": "did:web:...",
  "kycStatus": "approved",
  "kycLevel": "basic",
  "kycProvider": "shuftipro",
  "country": "GH",
  "kycCreateToken": "...",
  "expiresInSec": 900
}

kycCreateToken appears only once kycStatus is approved. It is one-time and expires in 15 minutes: it authorizes exactly one free account creation, and replaying it is rejected. Fetch it immediately before step 3 rather than storing it.

If status stays pending after you finished the provider UI on sandbox, contact support with your x-correlation-id — do not start a second verification while waiting.

This endpoint needs the kyc_write scope, not read — polling for an approval requires the same scope that started the verification.

3. Create the account

curl -X POST "$BI_URL/v1/accounts/free" \
  -H "Authorization: Bearer $BI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
        "ownerPublicKey": "PUB_K1_...",
        "personDid": "'"$PERSON_DID"'",
        "kycCreateToken": "'"$KYC_CREATE_TOKEN"'",
        "deviceFingerprint": "optional-device-hash",
        "preferredName": "myuser1"
      }'

Returns 201 with accountDid, the chain account name, and sponsored resources. On sandbox the chain is simulated — nothing is spent on a live network.

ownerPublicKey is the user's key — from a passkey or WharfKit session. We never see the private key. Optional fields worth sending:

FieldWhy
preferredNameDesired chain name, [a-z1-5.]{1,12}. Check GET /v1/accounts/name-available first
deviceFingerprintOptional opaque device signal your app already collects
activePublicKeyIf you want a separate active key from the owner key
emailClaimant contact; stored hashed

Sandbox does not require contact OTP for free-create. Production may additionally require emailProofToken and phoneProofToken from the OTP endpoints — a 403 here with KYC_REQUIRED, or a validation error naming a proof token, means your organization's policy expects one.

4. Bind the person to the account

The user proves they hold the account key by signing a challenge.

curl -X POST "$BI_URL/v1/bind/challenge" \
  -H "Authorization: Bearer $BI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accountDid":"'"$ACCOUNT_DID"'","personDid":"'"$PERSON_DID"'"}'

Returns { "message": { "nonce": "...", ... }, "payloadToSign": "..." }.

Have the user sign payloadToSign verbatim with the key matching ownerPublicKeysession.signArbitrary(payloadToSign) in WharfKit, or your passkey flow. Then confirm:

curl -X POST "$BI_URL/v1/bind/confirm" \
  -H "Authorization: Bearer $BI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "accountDid": "'"$ACCOUNT_DID"'",
        "personDid": "'"$PERSON_DID"'",
        "nonce": "'"$NONCE"'",
        "signature": "SIG_K1_..."
      }'

Nonces are single-use and expire. NONCE_INVALID means request a fresh challenge; INVALID_SIGNATURE means the signature did not verify against the account's registered key — usually the payload was modified or the wrong key signed.

This issues a binding credential, Ed25519-signed by our issuer. On sandbox the issuer DID is did:web:sandbox.identityinc.io (see /.well-known/did.json). Production uses a different issuer — credentials do not verify across environments.

5. Unlock

Finalize the account so the user holds it outright.

curl -X POST "$BI_URL/v1/accounts/unlock" \
  -H "Authorization: Bearer $BI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accountDid":"'"$ACCOUNT_DID"'"}'

ACCOUNT_NOT_PROVISIONAL here usually means a previous call already succeeded — check GET /v1/accounts/{did} and read lifecycle before retrying.

Check your work

curl -s "$BI_URL/v1/accounts/$ACCOUNT_DID" -H "Authorization: Bearer $BI_KEY"

Before you go live

Worth getting right on sandbox before production traffic:

  • Verifications are billed per start on production. Keep sending an Idempotency-Key on /v1/kyc/start — see Idempotency.
  • Free account creation is rate limited on production. Build the retry path against 429 before you load-test.

Switch BI_URL / BI_KEY to production only when you are ready for live checks.

Next

Quickstart · Identity Inc.