← Documentation

Authentication

API keys, scopes, HMAC request signing, IP allowlists, and idempotency.

Every call to the Identity Inc. API is authenticated with an organization API key. Institutional callers can layer on request signing and an IP allowlist.

API keys

Send the key as a bearer token on every request:

# Production
curl https://api.identityinc.io/v1/tenant \
  -H "Authorization: Bearer bi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Sandbox (separate deployment — use a bi_test_ key on this host only)
curl https://sandbox.identityinc.io/v1/tenant \
  -H "Authorization: Bearer bi_test_xxxxxxxxxxxxxxxxxxxxxxxx"
EnvironmentPrefixWhere the key comes from
Productionbi_…Signup, or POST /v1/admin/api-keys
Sandboxbi_test_…POST https://sandbox.identityinc.io/v1/billing/checkout (Builder) — not the marketing checkout

Only a SHA-256 hash is stored, so a key is shown once — at signup / sandbox checkout, or when you mint one at POST /v1/admin/api-keys. If you lose it, revoke and issue another; there is no recovery path.

The key identifies your organization. Everything it touches — accounts, people, audit entries, quota — is scoped to that organization and invisible to others.

Server-side only. A key in browser JavaScript is a public key. Call the API from your backend and expose your own endpoints to the browser.

Missing or malformed header401 UNAUTHORIZEDMissing Bearer API key
Revoked, expired, or unknown key401 UNAUTHORIZED
Valid key, wrong scope403 FORBIDDEN

Scopes

A key carries a set of scopes, chosen when it is created. Requesting an endpoint outside them returns 403 — before any side effect, so a rejected call never half-runs.

ScopeGrants
readGET /v1/accounts/{did}, /v1/accounts/name-available, /v1/accounts/{did}/contact, /v1/tenant, /v1/metrics
accounts_writePOST /v1/accounts/free, contact + OTP endpoints
kyc_writePOST /v1/kyc/start, /v1/kyb/start, and GET /v1/kyc/status
bind_writePOST /v1/bind/challenge, /v1/bind/confirm
unlock_writePOST /v1/accounts/unlock
compliance_export/v1/compliance/* — export jobs, artifacts, audit verify
webhook_manage/v1/admin/webhooks* — register, list, deactivate, replay
admin/v1/admin/* — keys, signing, IP allowlist, country policy, DPA, suspend

Issue narrow keys per workload. A key that only creates accounts cannot export your audit log if it leaks.

Note that GET /v1/kyc/status sits under kyc_write, not read — polling for an approval needs the same scope that started the verification, so a read-only key cannot drive the KYC flow on its own.

curl -X POST https://api.identityinc.io/v1/admin/api-keys \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"signup-service","scopes":["accounts_write","kyc_write","bind_write","unlock_write"]}'

Request signing (HMAC)

Optional, and off by default. Once enabled for your organization, every authenticated request must be signed or it is refused — enable it from a deploy that already signs, not before.

curl -X POST https://api.identityinc.io/v1/admin/signing/enable \
  -H "Authorization: Bearer $ADMIN_KEY"      # returns the signing secret, once

Add two headers:

HeaderValue
X-BI-TimestampCurrent Unix time in milliseconds
X-BI-SignatureHex HMAC-SHA256 over the canonical string below

The canonical string is four dot-joined fields — note that it covers the method and path, and the SHA-256 of the body rather than the body itself:

{timestampMs}.{METHOD}.{path}.{sha256hex(rawBody)}
  • METHOD is uppercase (POST, not post).
  • path is the path as sent, with no origin and no query string rewriting.
  • rawBody is the exact bytes you transmit. Serialize once and send that same string — re-serializing between signing and sending changes the hash.
  • For a request with no body, hash the empty string.
import { createHash, createHmac } from "node:crypto";

function signedHeaders(secret: string, method: string, path: string, body = "") {
  const ts = Date.now();
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const canonical = `${ts}.${method.toUpperCase()}.${path}.${bodyHash}`;
  return {
    "X-BI-Timestamp": String(ts),
    "X-BI-Signature": createHmac("sha256", secret).update(canonical).digest("hex"),
  };
}

const body = JSON.stringify({ accountDid });      // serialize once…
await fetch(baseUrl + "/v1/accounts/unlock", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    ...signedHeaders(secret, "POST", "/v1/accounts/unlock", body),
  },
  body,                                            // …and send that same string
});

Signatures are single-use. Beyond the timestamp window, each signature is burned after one request, so a captured call cannot be replayed inside the window. Retrying a failed request means signing it again with a fresh timestamp — do not cache and resend headers.

Timestamps must be within 5 minutes of server time. Run NTP; clock drift on one host presents as intermittent 401s that look like a key problem.

FailureResponse
Header missing while signing is required401Signed requests required (X-BI-Timestamp, X-BI-Signature)
Timestamp unparseable401Invalid timestamp
Clock more than 5 minutes out401Request timestamp skew too large
Signature does not match401Invalid request signature
Signature already used401Request signature already used (replay)

Outbound webhooks use the same two header names with a different canonical string{timestamp}.{rawBody}, no method, path, or body hash. Do not reuse this signing function to verify incoming webhooks. See Webhooks.

IP allowlists

Restrict a key to known egress addresses:

curl -X PUT https://api.identityinc.io/v1/admin/ip-allowlist \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cidrs":["203.0.113.0/24","198.51.100.7/32"]}'

An allowlist fails closed: once set, every request from an address outside it returns 403, including the one that would fix the list. Ask the API what address it actually sees before you set one — behind NAT or an egress gateway it is rarely the address you expect:

curl -s https://api.identityinc.io/v1/whoami   # {"clientIp":"203.0.113.42"}

Run that from the server that will hold the key, not your laptop. Include every egress address your platform can use, not just the one that answered today.

Idempotency

POST /v1/accounts/free, POST /v1/kyc/start, and POST /v1/billing/checkout each cost real money or resources. Send an Idempotency-Key so a retry after a dropped connection or proxy timeout replays the original response instead of charging again:

curl -X POST https://api.identityinc.io/v1/kyc/start \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 7f3c1a9e-2b44-4c0d-9e51-2a8f0d6b1c33" \
  -H "Content-Type: application/json" \
  -d '{}'
  • Use a fresh unique value per logical operation — a UUID per signup attempt.
  • Keys are at most 255 characters and scoped to your organization.
  • Replays are answerable for 24 hours; after that the key is purged and the same value starts a new operation.
  • Reusing a key with a different body is rejected rather than silently returning the first response, because that is a client bug either way.

Correlation IDs

Pass x-correlation-id on any request and it is echoed back and written into the server logs for that call. Supply your own request ID and a support conversation can start from your trace instead of a timestamp guess. If you omit it, one is generated.

See also

  • Errors — every error code and what to do about it
  • Webhooks — events, verification, retries
  • Quickstart — first verified account in ten minutes
  • OpenAPI spec — the machine-readable contract
Authentication · Identity Inc.