idcheck.now API Docs

Authentication

idcheck.now uses two credential types: scoped API keys for your backend, and short-lived client tokens for the end-user's browser. Webhook deliveries are signed with HMAC-SHA256 so you can verify they came from us.

API keys

API keys authenticate server-to-server calls. Send the key in the X-Api-Key header:

curl
curl https://idcheck.now/v1/verifications -H "X-Api-Key: idc_9fK2mVxQ8pLrT3nWsYbZcJhGdAeRuEiO7qN5sX0vM1k"

Keys are generated with the prefix idc_ followed by 32 bytes of URL-safe randomness. Only the SHA-256 hash is stored server-side — the plaintext key is shown exactly once, at creation time, and can never be retrieved again. A missing, unknown or revoked key returns 401 invalid API key; failed attempts are audit-logged.

Scopes

Each key carries a list of scopes. A request whose key lacks the required scope returns 403 missing scope: <scope>. The wildcard scope * grants everything.

ScopeGrants
verifications:writePOST /v1/verifications
verifications:readGET /v1/verifications, GET /v1/verifications/{id}, GET /v1/verifications/{id}/media
screen:ofacPOST /v1/screen/ofac, POST /v1/screen/wallet, GET /v1/screen/ofac/stats
keys:writePOST /v1/keys, POST /v1/keys/{id}/revoke
keys:readGET /v1/keys
webhooks:writePOST /v1/webhooks, DELETE /v1/webhooks/{id}
webhooks:readGET /v1/webhooks
*All of the above.
Principle of least privilege

Give your capture service only verifications:write, your dashboard backend only verifications:read, and keep keys:write on a single bootstrap key stored in a secrets manager. Session endpoints (/v1/session/*), the free tier (/v1/free/*) and signal collection (/v1/signals/collect) do not use API keys at all.

Creating keys

Create keys in the dashboard, or programmatically. To create a key, POST /v1/keys with a label and a scopes array (requires keys:write):

curl
curl -X POST https://idcheck.now/v1/keys -H "X-Api-Key: $BOOTSTRAP_KEY" -H "Content-Type: application/json" -d '{"label": "staging", "scopes": ["verifications:write", "verifications:read"]}'
JavaScript
const res = await fetch("https://idcheck.now/v1/keys", {
  method: "POST",
  headers: { "X-Api-Key": process.env.BOOTSTRAP_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ label: "staging", scopes: ["verifications:write", "verifications:read"] }),
});
const { api_key } = await res.json(); // shown once — store it now
Python
import os, requests

res = requests.post(
    "https://idcheck.now/v1/keys",
    headers={"X-Api-Key": os.environ["BOOTSTRAP_KEY"]},
    json={"label": "staging", "scopes": ["verifications:write", "verifications:read"]},
)
api_key = res.json()["api_key"]  # shown once — store it now
JSON201 Created
{
  "id": 12,
  "label": "staging",
  "scopes": ["verifications:write", "verifications:read"],
  "api_key": "idc_9fK2mVxQ8pLrT3nWsYbZcJhGdAeRuEiO7qN5sX0vM1k"
}

List keys with GET /v1/keys (requires keys:read) and revoke with POST /v1/keys/{key_id}/revoke (requires keys:write). Neither ever returns key material:

JSONGET /v1/keys — 200 OK
[
  {
    "id": 12,
    "label": "staging",
    "scopes": ["verifications:write", "verifications:read"],
    "created_at": "2026-07-26T09:12:44.012331",
    "revoked_at": null,
    "last_used_at": "2026-07-26T14:22:03.554201"
  }
]

Revoke returns the same public object with revoked_at set; revoked keys immediately fail authentication. Revoking an already-revoked key is idempotent.

Client tokens

A client token is an opaque, single-verification credential that powers the hosted capture flow. It is created for you by POST /v1/verifications and returned as client_token (also embedded in the hosted url).

  • TTL: 30 minutes (1800 seconds). After expiry the user must be given a fresh verification.
  • Scope: exactly one verification — a token can never see another user's data.
  • Headers: send as X-Client-Token, or as Authorization: Bearer <token>.
curl
curl https://idcheck.now/v1/session/state -H "X-Client-Token: x7Qp9m2LwR4tY8uZcVbN1sDfGhJkL0oP3eA5qW6rT2y"

Invalid or expired tokens return 401 invalid or expired client token. Never put an API key in browser code — the client token is the only credential the browser ever needs.

Webhook signatures

Every webhook delivery carries an X-IdCheck-Signature header:

http
X-IdCheck-Signature: t=1753104000,v1=5b7c9e1f3a8d2c4b6e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d

The value is t=<unix_timestamp>,v1=<hmac_hex>, where hmac_hex is HMAC-SHA256 over the string "<t>.<raw_request_body>" using your endpoint's secret (returned once at webhook creation) as the key. Verify with a constant-time comparison, and reject timestamps that are too old to defend against replay:

JavaScript
const crypto = require("crypto");

function verifyIdCheckSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const ts = Number(parts.t);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.` + rawBody)   // raw body string/Buffer — do NOT re-serialize parsed JSON
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
Python
import hashlib, hmac, time

def verify_idcheck_signature(raw_body: bytes, header: str, secret: str,
                             tolerance_sec: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = int(parts.get("t", "0"))
    if abs(time.time() - ts) > tolerance_sec:
        return False
    expected = hmac.new(
        secret.encode(), str(ts).encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
Verify against the raw body

HMAC input is the exact bytes received. Parsing the JSON and re-serializing it (different key order, whitespace or unicode escaping) will produce a different digest and every signature will fail.