API
API
Create a verification session, send the applicant to the hosted link, receive the signed webhook, fetch the result.
The machine-readable reference is GET /api/v1/openapi.json (OpenAPI 3.1, webhooks included); the readable one is the API reference.
Tokens: live and test
- Personal tokens. A token acts as its owner in a team (
X-Team-Id), so an integration stops working when that user leaves the team. Use a dedicated user for production integrations. - Abilities:
sessions:read,sessions:write,flows:read,webhooks:read(plus the account ones,read/write). The token's team role still applies: erasing a session needs owner or admin. - Mode (chosen at creation, Account, API tokens):
tid_live_...works on real sessions only;tid_test_...works on test sessions only (livemode: false). A session id of the other mode is a404. - Test mode: the hosted link shows a TEST banner and a simulator (approve, reject with a reason, request a
resubmission) instead of real captures. Test sessions never enter the review queue, never reach Pumble, are never billed,
send no email, and the record is deleted 24 hours after its last activity. Webhooks fire normally with
"livemode": false. - The dashboard has a Live / Test switch on Sessions and shows a TEST badge.
Quick start
# 1. Create a session (test token while you integrate)
curl -sS https://YOUR-LEGITFLASH-DOMAIN/api/v1/sessions \
-H "Authorization: Bearer $TRUSTID_TOKEN" \
-H "Idempotency-Key: order-1234-verification" \
-H "Content-Type: application/json" \
-d '{"flow_id":"01J...","external_reference":"order-1234","metadata":{"order":"1234"},"redirect_url":"https://shop.example/verified"}'
# -> 201 {"data":{"id":"01J...","status":"created","url":"https://YOUR-LEGITFLASH-DOMAIN/v/...","qr_svg_url":"...","embed":{...}}}
# 2. Redirect the applicant to data.url (or show the QR, or embed it), then wait for the webhook.
# 3. Fetch the result
curl -sS https://YOUR-LEGITFLASH-DOMAIN/api/v1/sessions/01J... -H "Authorization: Bearer $TRUSTID_TOKEN"
// Node 22
const res = await fetch('https://YOUR-LEGITFLASH-DOMAIN/api/v1/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TRUSTID_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': `order-${order.id}-verification`,
},
body: JSON.stringify({ flow_id: process.env.TRUSTID_FLOW, external_reference: String(order.id) }),
});
const { data } = await res.json();
redirect(data.url); // shown only in this response: store data.id, not the link
// PHP (Laravel HTTP client)
$session = Http::withToken(config('services.trustid.token'))
->withHeaders(['Idempotency-Key' => "order-{$order->id}-verification"])
->post('https://YOUR-LEGITFLASH-DOMAIN/api/v1/sessions', [
'flow_id' => config('services.trustid.flow'),
'external_reference' => (string) $order->id,
])->throw()->json('data');
return redirect($session['url']);
Webhooks: see Webhooks (signature, retries, payload; the body also carries livemode and your metadata).
Endpoints
| Method | Path | Ability | Notes |
|---|---|---|---|
POST |
/sessions |
sessions:write |
201 + the session and url, qr_svg_url (signed, 15 min), embed. Idempotency-Key honoured |
GET |
/sessions |
sessions:read |
cursor pagination; filters status, flow_id, external_reference, `created[gte |
GET |
/sessions/{id} |
sessions:read |
status, decision, reason codes, result (per the flow's result detail), check flag names, timeline, media_purge_at. Never media |
POST |
/sessions/{id}/cancel |
sessions:write |
only before submission; else 409 not_cancellable |
POST |
/sessions/{id}/link |
sessions:write |
new hosted link, the old one dies; 409 not_relinkable once decided/expired |
DELETE |
/sessions/{id} |
sessions:write |
data-subject erasure now (media, record, webhook payloads), 204 |
GET |
/sessions/{id}/export |
sessions:read |
access-request export (JSON, no media binaries, no internal notes) |
GET |
/flows, /flows/{id} |
flows:read |
`archived=0 |
GET |
/usage |
sessions:read |
period, included (the plan's verifications per month, or the trial allowance), used, overage (extra ones beyond included), cap (most extra verifications the team's overage cap allows, null = no cap), overage_allowed |
GET |
/webhook-deliveries |
webhooks:read |
session_id, status; delivery status only, never payloads |
Create body: flow_id (required), external_reference, applicant.email, locale (en|es), send_invite (live mode only),
redirect_url (its host must be one of the hosts of the flow's redirect URLs), metadata (up to 10 string pairs, 500 bytes
each, keys [A-Za-z0-9_.-]{1,40}). Metadata must never contain personal data: it is echoed in responses and webhooks and kept as plain text.
Idempotency
Send Idempotency-Key (at most 255 characters) on any POST. Per team and token mode, for 24 hours: the same key with the
same request replays the first 2xx response (header Idempotent-Replayed: true, even the one-time url); the same key with a different
request is 422 idempotency_key_reused. Failed requests are not stored, so a corrected retry just runs. Only a hash of the key is
stored and the stored response is encrypted.
Errors, pagination, limits, versioning
- Errors are RFC 9457
application/problem+jsonwith a stablecode(validation_failed,missing_ability,not_found,flow_not_ready,quota_exceeded(HTTP 402: no plan, trial used up, included used up with overage off, or overage cap reached;quota.reasonandquota.used/includedsay which),not_cancellable,not_relinkable,idempotency_key_reused,idempotency_key_invalid,rate_limited, ...). More in Errors. - Lists use cursor pagination: follow
links.nextuntil it isnull. - Rate limit per token by plan (
limits.api_rpm):X-RateLimit-Limit/Remaining,429+Retry-After. - A token in the query string is a
400; always use theAuthorizationheader. - Versioning: changes inside
v1are additive (new fields, endpoints, events, enum values: ignore what you do not know). A breaking change ships as/api/v2with a 12-month overlap, announced byDeprecationandSunsetheaders onv1. - Developers, API in the dashboard shows the last 100 calls of the team (method, path, status, duration, token name; no bodies).