API reference
Every /v1 door, its refusals, and the webhook envelope
Get started with the API
The /v1 API is what a customer’s backend calls with an API key: verification sessions, the directory, events, check, screening, exceptions and approvals. Every /v1 response carries Hira-Request-Id (req_ and 32 hexadecimal characters) and Cache-Control: no-store, and no /v1 door answers CORS. The /v1 routes carry no version: a response change reaches every caller when the server deploys.
Authentication
Every /v1 door reads the key from one header:
Authorization: Bearer hira_sk_test_…
A key is hira_sk_live_ in a live account or hira_sk_test_ in its sandbox account, then 43 base62 characters, an underscore and a 6-character CRC32 checksum, so a mistyped key is refused without a lookup. Keys are created under Administration › Developer › API keys and shown once. A missing, malformed, revoked or rotated-out key, or a locked account’s key, answers 401 api_key_invalid with one body for every case. An address that sends 30 requests with a missing or invalid key in one minute answers 429 to every request for the rest of that minute, a valid key’s included.
Rotating a key creates a new key with the same scopes and app; the old key keeps working for 7 days, then stops. Revoking a key stops it at once.
Scopes
A key reaches only the products ticked in its Scopes when it was created. Scopes are fixed once the key exists; a rotated key keeps them. A key without the door’s product answers 403 api_key_scope_missing; an account whose plan lacks the product answers 403 product_not_enabled.
| Scope | Console label | Doors |
|---|---|---|
| verification | Identity verification | /v1/verification-sessions and every door below it. |
| directory | Directory | /v1/directory/users, /v1/directory/businesses. |
| monitoring | Transaction monitoring | /v1/events, /v1/check, /v1/screening/address/{address}, /v1/exceptions, /v1/approvals, /v1/reset. |
Errors
A refusal answers a JSON envelope: error is a sentence for a person, code a stable word for a program, and param, present only when the refusal is about one request field, names that field (dotted below the root, such as simulate.outcome). A code stays as written and answers one status; its sentence may change. A later version may add a code, so a branch on code keeps a default arm.
{
"error": "A link lasts between 10 minutes and 7 days.",
"code": "expires_in_out_of_range",
"param": "expiresIn"
}
| Code | Status | When |
|---|---|---|
| request_invalid | 400 | The body is not a JSON object, or arrives under another media type or none. “The request body must be a JSON object.” |
| unknown_field | 400 | The body names a field the door does not declare, nested ones included; param names it. |
| field_required | 400 | A required field is missing. |
| field_invalid | 400 | A field is not in the shape the door takes. |
| customer_ref_invalid | 400 | A session create or list reference breaks the customer-reference rule: 1 to 120 letters, digits and . _ : / -. The directory and events doors answer the same rule with field_invalid today. |
| customer_ref_personal_data | 400 | A session create or list reference contains @ or starts with + (the directory and events doors answer field_invalid). |
| metadata_invalid | 400 | Metadata past its limits, holding a control character, or holding a value that looks like an email address or a phone number. |
| expected_details_invalid | 400 | Expected details hold a control character, or the details on record cannot be compared against. |
| expected_details_required | 400 | The flow screens the person or the company and neither the request nor the party record holds what it needs. |
| return_url_invalid | 400 | returnUrl is not an absolute https URL of at most 2048 characters with no userinfo. |
| return_url_origin_not_allowed | 400 | Any well-formed returnUrl: no account holds an allowed origin today. |
| expires_in_out_of_range | 400 | expiresIn outside 600 to 604800 seconds. |
| simulate_not_allowed | 400 | Simulate on a live account. |
| idempotency_key_invalid | 400 | Idempotency-Key is not 1 to 255 printable ASCII characters. |
| filters_not_combinable | 400 | A list names more than one filter, or both cursors. |
| event_type_retired | 400 | identity.check.result sent to /v1/events or /v1/check. |
| api_key_invalid | 401 | The key is missing or not valid. |
| api_key_scope_missing | 403 | The key does not reach the door’s product. |
| product_not_enabled | 403 | The account’s plan does not include the product. |
| account_suspended | 403 | The account is suspended or not active. |
| engine_not_connected | 403 | A live key creating a verification session. |
| session_not_found | 404 | No session of the key’s account has that id. An id in another account answers this too, never 403. |
| program_not_found | 404 | No verification program of the account has that id, the program subscribes to streams, or it is archived. |
| program_not_active | 409 | The program is paused, its flow cannot be read, or a step of its flow has no policy that judges it. |
| party_kind_mismatch | 409 | The customerRef names a party of the other kind. |
| party_unavailable | 409 | The party is blocked or was merged into another record. |
| session_state_invalid | 409 | The session’s state does not allow the verb. |
| daily_quota_reached | 409 | A sandbox account made 500 verification sessions in the UTC day. |
| request_too_large | 413 | The body is larger than the route accepts. “The request body is larger than this route accepts.” |
| idempotency_key_reused | 422 | An Idempotency-Key already used with another body. |
| rate_limited | 429 | A window is spent; Retry-After says when to retry. |
| internal_error | 500 | “An unexpected error occurred.” |
| service_unavailable | 503 | A party changed while a directory create or a session create was registering it; nothing was written and a resend lands. “The party changed while it was being registered and was not written. Send the request again.” |
The SDK retries 429, 500, 503 and a network error; every other code is final for that request. GET /v1/approvals/{id} answers its 404 as { "error": "Decision not found." } with no code.
Rate limits
Each key has its own one-minute windows, and an account’s keys share one more. A spent window answers 429 rate_limited with a Retry-After header in whole seconds, at least 1.
| Window | Per minute | Doors |
|---|---|---|
| create, per key | 300 | Session create, cancel, links and simulate; the directory creates. |
| read, per key | 600 | Session get and list; screening, exceptions, approvals. |
| ingest, per key | 300 | /v1/events, /v1/check, /v1/reset. |
| account, all keys | 1,200 | Every key-authed door. |
| invalid key, per address | 30 | Requests with a missing or invalid key. |
A sandbox account also creates at most 500 verification sessions per UTC day; the next answers 409 daily_quota_reached.
Idempotency
POST /v1/verification-sessions takes an Idempotency-Key header of 1 to 255 printable ASCII characters. The key is scoped to the account and the key’s source app, kept for 24 hours, and stored only once a create has passed every rule, so a refused create leaves it free for the corrected retry.
- The same key with the same body answers
201with the original session, nourl, and the headerIdempotent-Replayed: true. The body is compared in canonical form, so reordered keys or changed whitespace still match. - The same key with a different body answers
422idempotency_key_reused. - Two creates racing on one key make one session; the second answers as a replay.
The SDK sends a key with every create and reuses it on its own retries. /v1/check and /v1/events carry their key in the event envelope instead, and the directory creates use customerRef as theirs.
Session object
Create, list, cancel, links and simulate answer the session in this shape. Words in an enumerated field may grow in a later version.
| Field | Type | Description |
|---|---|---|
| id | string | vsn_ and 32 hexadecimal characters. |
| object | "verification_session" | The object’s name, the same word a webhook’s data.object carries. |
| livemode | boolean | false on a sandbox account. |
| status | string | created, in_progress, completed, expired, abandoned, canceled, error. Past expiresAt, a sweep that runs every minute stamps a created session expired and an in-progress one abandoned. A completed session can reopen to in_progress. |
| decision | string | null | approved, declined, in_review; null while the session is open. |
| assurance | string | null | measured when a source checked the person, simulated when the outcome was asked for in the sandbox. |
| seq | integer | The session’s change counter, 0 at create, raised by each change of the session (a move to error included). An announced change carries the new value; gaps are normal. |
| origin | string | api (this door), link (a program’s shared link) or operator (started in the console). |
| programId | string | The program whose flow the person walks. |
| flowRevision | integer | The revision of the program’s flow the session walks. |
| customerRef | string | null | The account’s own identifier for the party. |
| clientReference | string | null | The account’s own reference for this session. |
| partyId | string | null | The directory party the session names. |
| returnUrl | string | null | Null today; see return_url_origin_not_allowed. |
| metadata | object | null | The string pairs sent at create. |
| redaction | object | null | { "status": "completed", "redactedAt" } once the applicant’s data was erased in the console. |
| createdAt, startedAt, completedAt, expiresAt, canceledAt | string | null | ISO 8601 instants in UTC; createdAt and expiresAt are always set. |
| url | string | The applicant link, on a fresh create and on /links alone; absent everywhere else. |
GET /v1/verification-sessions/{id} adds:
| Field | Type | Description |
|---|---|---|
| decisionDetail | object | null | declineType (retry, final), decidedBy (officer, policy, system), decidedAt, decisionId, reason (too_many_cases, unjudged_step, party_details_differ, step_outcome), caseIds, and reasons: each step that concluded failed or unable-to-verify as { stepKey, outcome, causes }. |
| errorDetail | object | null | { code }: why a session stands in error. |
| expectedDetailsSupplied | boolean | Whether the session holds expected details, sent at create or taken from the party record. The details themselves never come back. |
| applicant | object | null | { country, subdivision } as the create stated them. |
| steps | array | Each concluded step’s latest attempt: stepKey, outcome (verified, failed, unable-to-verify), assurance, attempt, concludedAt, notChecked ([{ measure, cause }]). No signal and no score. |
| pendingSteps | string[] | The step keys still due. |
| cases | array | Cases raised on the walk: { id, status, decisionIds }, the status in lower case. |
| consent | object | null | { faceStep, decidedAt }: the latest biometric consent decision (granted, refused, withdrawn). Never the notice’s text. |
| riskRating | string | null | low, medium or high, as an analyst stated it when resolving the walk’s case; a dismissed or accepted case’s rating is not answered. |
No session answer carries the url (outside create and /links), the expected details, a name, a date of birth, a document number, an image or a consent’s text.
POST /v1/verification-sessions 201 · scope verification · 16 KiB
Opens a session on a hosted program of the key’s account. Today a sandbox key alone creates: a live key answers 403 engine_not_connected, “Live verification sessions open once an engine is connected. Sessions are created in the sandbox account with a test key.” The body is strict: a field not listed answers 400 unknown_field.
| Field | Type | Description |
|---|---|---|
| programId required | string | A hosted, active program of the account, in any of its workspaces. The session takes the program’s workspace and its current flow revision. |
| customerRef required | string | The account’s own identifier for the party, under the customer-reference rule. It names the party in the directory: a party of the flow’s kind is reused, else one is registered. A flow that declares no kind verifies a person. |
| clientReference optional | string | The account’s own reference for this session, under the same rule. |
| expectedDetails optional | object | What the person should match: fullName and dateOfBirth (YYYY-MM-DD), or a business’s legalName, registrationNumber and country. Stored, never answered. A flow that screens the person or company needs them here or on the party record, else 400 expected_details_required. |
| applicant optional | object | country (ISO 3166-1) and subdivision (ISO 3166-2): where the applicant is. |
| expiresIn optional | integer | Seconds the link lasts, 600 to 604800; 259200 (72 hours) unset. |
| metadata optional | object | At most 16 string pairs, keys up to 40 characters, values up to 500, 4,096 bytes as stored. No value may look like an email address or a phone number. |
| simulate optional | object | Sandbox only: { "outcome": "verified" | "failed" | "unable-to-verify" }. The hosted page opens each step on that outcome. |
| returnUrl optional | string | Refused today: 400 return_url_origin_not_allowed. |
Header Idempotency-Key, optional; see Idempotency. Answers 201 with the session and its url. Refusals, in the order a create meets them: 400 request_invalid, unknown_field, field_invalid (a body that does not bind); 403 product_not_enabled, engine_not_connected; 400 field_required, customer_ref_invalid, customer_ref_personal_data, expected_details_invalid (a control character), expires_in_out_of_range, metadata_invalid, field_invalid (simulate.outcome), return_url_invalid, return_url_origin_not_allowed, idempotency_key_invalid; 422 idempotency_key_reused; 404 program_not_found; 400 expected_details_invalid and field_invalid (the details’ or the applicant’s shape); 403 account_suspended; 409 program_not_active, daily_quota_reached, party_kind_mismatch, party_unavailable; 503 service_unavailable (the party changed while it was being registered; send the request again); 400 expected_details_required.
GET /v1/verification-sessions/{id} 200 · scope verification
The session with its detail fields. Past its deadline a session reads expired or abandoned once the minute sweep has stamped it. Refusal: 404 session_not_found, “No verification session of this account has that id.” A plan without Identity verification still answers this read.
GET /v1/verification-sessions 200 · scope verification
The account’s sessions newest first, whichever door opened them, as { "object": "list", "data": [...], "hasMore": boolean }. No totals and no status filter.
| Query | Type | Description |
|---|---|---|
| customerRef | string | Sessions of one party reference. |
| clientReference | string | Sessions under one session reference. |
| programId | string | Sessions of one program. |
| createdAfter | string | An ISO 8601 date or instant; without an offset it reads as UTC. |
| limit | integer | 1 to 100; 25 unset. |
| startingAfter | string | A session id from a page: the page after it. |
| endingBefore | string | A session id from a page: the page before it. |
At most one of customerRef, clientReference and programId, and never both cursors: else 400 filters_not_combinable. A malformed programId, limit or createdAfter, or a cursor that names no session of the account, answers 400 field_invalid naming it.
POST /v1/verification-sessions/{id}/cancel 200 · scope verification · 1 KiB
No body, or an empty one. Cancels a session that is created, in_progress, expired or abandoned; a canceled session answers unchanged. A suspended account still cancels. Refusals: 404 session_not_found; 409 session_state_invalid for completed (“This verification link has already been completed, so there is nothing to cancel.”) and error.
POST /v1/verification-sessions/{id}/links 200 · scope verification · 1 KiB
Mints a fresh applicant link and answers the session with its new url. Body optional: { "expiresIn": seconds }, 600 to 604800, 72 hours unset. The previous link stops working and the deadline moves. An expired session becomes created and an abandoned one in_progress, with seq raised and one verification_session.reopened event; an open session keeps its seq. A retry after a lost answer mints a second link to the same session.
Refusals: 404 session_not_found; 409 session_state_invalid for a completed, canceled, errored or redacted session, and for a session opened through a program’s shared link; 403 account_suspended; 404 program_not_found (the program is archived or not a verification program); 409 program_not_active (paused); 400 expires_in_out_of_range.
POST /v1/verification-sessions/{id}/simulate 200 · scope verification · sandbox · 1 KiB
Body { "outcome": "verified" | "failed" | "unable-to-verify" }. Concludes every remaining step of the session with that outcome through the recorder the hosted page posts to, one step at a time, so each step is staged, evaluated and settled as a walk’s is, with assurance simulated. A face step with no granted consent concludes unable-to-verify with every measure not run. Answers the session.
Refusals, in order: 404 session_not_found; 400 simulate_not_allowed on a live account (“simulate is accepted in the sandbox account alone.”); 400 field_invalid for another outcome (“outcome is one of ‘verified’, ‘failed’ or ‘unable-to-verify’.”); 409 session_state_invalid when the session is not created or in_progress or is past its deadline (“This session is not open, so its steps cannot be simulated.”); 403 account_suspended; 404 program_not_found when the session’s program is archived or not a verification program; 409 program_not_active when it is paused (“This session’s program is paused, so its steps cannot be simulated.”).
Webhooks
Endpoints are registered in the console under Administration › Developer › Webhooks, on live and sandbox accounts alike; there is no /v1 door for them. An endpoint holds a URL, a description, an explicit list of event types (at least one) and the API version 2026-10-01. An account holds at most 10 endpoints, disabled ones included. An endpoint is created enabled, and its signing secret, hira_whsec_ and 32 random bytes, is shown once at create and at each roll. Send test event posts a test delivery and shows the receiver’s answer.
An endpoint URL is https on port 443, with no user name, password or fragment, at most 2048 characters, and not under a Hira domain. Redirects are never followed. Every address the name resolves to is checked at create, at a URL change and at every attempt, and private and special-purpose addresses are refused.
| Event type | Sent when |
|---|---|
| verification_session.decided | A decision is set or changes; an in_review session later approved sends a second one. |
| verification_session.reopened | A completed, expired or abandoned session opens again: a new link, a late step, an officer’s request for more, or a retry decline. |
| verification_session.expired | A session that never started passes its deadline. |
| verification_session.abandoned | A started session passes its deadline. |
| verification_session.canceled | A session is canceled. |
A session moving to error sends no event. The body is the same bytes on every attempt and resend:
| Field | Type | Description |
|---|---|---|
| id | string | evt_…; evt_test_… on a test send. Dedupe on it. |
| object | "event" | |
| type | string | One of the five types above. |
| apiVersion | string | 2026-10-01. |
| createdAt | string | When the change was recorded, ISO 8601 in UTC with microseconds. |
| livemode | boolean | false for a sandbox account’s events. |
| data | object | { id, object: "verification_session", seq }: the session’s id and its seq at this event. A test send’s data.id starts vsn_test_ and names no session. |
Each delivery is a POST with Content-Type: application/json, User-Agent: Hira-Webhooks/1.0, Hira-Event-Id (the body’s id), Hira-Test-Webhook: true on a test send, and the signature:
Hira-Signature: t=1760242743,v1=5f2b…
t is unix seconds and each v1 is the hex HMAC-SHA256, keyed by the signing secret, over t, a dot and the raw body. Each attempt carries a fresh t and signature. For 24 hours after a secret roll both secrets sign and the header carries two v1 values. constructEvent(rawBody, header, secret, { tolerance, now }) in @hira/sdk verifies it and throws HiraSignatureError, whose reason is malformed_header, no_signature, bad_signature, timestamp_outside_tolerance or bad_tolerance. The tolerance is 1 to 600 seconds, 300 unless set, and cannot be turned off; now is the clock it is held to, a function answering milliseconds since the epoch, Date.now unless set.
Retries. A delivery counts when the receiver answers 2xx within 10 seconds; a 3xx, any other status, a timeout or a refused address is a failed attempt. A live account’s delivery is tried at once, then after 1 m, 5 m, 30 m, 2 h, 6 h, 12 h and 24 h three times, about 92 h 36 m in all, each wait varying by up to 10 percent; it then reads failed. A sandbox account’s is tried three times within an hour. An endpoint with seven days of only failures is disabled with a reason, and the account’s administrators and owners are told. Deliveries are not ordered: read the session through the API, and drop an event whose data.seq is below the last one applied for that session. Hira’s sending addresses are not published; trust a delivery by its signature.
Directory
Registers the account’s users and businesses as parties. Scope directory, create window. A key writes parties and reads none. Both bodies are strict: a field not listed, vendorRef included, answers 400 unknown_field and writes nothing.
POST /v1/directory/users 200
| Field | Type | Description |
|---|---|---|
| displayName required | string | 1 to 200 characters, trimmed. |
| customerRef optional | string | The account’s own id for the person, under the customer-reference rule; the idempotency key. Blank reads as none. |
| status optional | string | Active (unset), InReview, Flagged or Blocked. A status already decided in the console on that reference stands. |
| dateOfBirth optional | string | YYYY-MM-DD. |
| email, phone optional | string | An email address up to 320 characters; a phone number up to 40. |
| address optional | object | line1, line2 (200), city, state, country (100), postalCode (20). Strict too. |
POST /v1/directory/businesses 200
| Field | Type | Description |
|---|---|---|
| legalName required | string | 1 to 200 characters, trimmed. |
| customerRef optional | string | As for users. |
| status optional | string | As for users. |
| registrationNumber optional | string | Up to 60 characters. |
| incorporationCountry optional | string | Up to 100 characters. |
| email, phone, address optional | — | As for users. |
Both answer a receipt and nothing else: { "id", "kind": "user" | "business", "customerRef", "created" }. A customerRef that already names a party of the same kind writes nothing and answers the same id with created: false; the values sent are not compared with the stored party. Refusals: 400 unknown_field and request_invalid; 400 field_invalid for any field rule, a customerRef outside the customer-reference rule included (the sentence says which); 403 product_not_enabled (“This account’s plan does not include the Directory API. Contact Hira to enable it.”); 409 party_kind_mismatch (“The customerRef already identifies a party of the other kind. One reference names a user or a business, never both.”); 503 service_unavailable when the party changed mid-registration and nothing was written. The SDK retries transient failures of a directory create only when a customerRef is set.
Events
POST /v1/events records activity after it happens. Scope monitoring, ingest window, 8 MiB body, at most 1,000 events a batch; a successful batch answers 202 with no body. Nine types are accepted. Each payload below is in addition to the common fields; unknown payload fields are kept on the event as sent. The required marks are the SDK’s types; the server enforces the envelope and the common-field rules. A batch with one refused event is refused whole.
mint Approved minter · reconciliation
| Field | Type | Description |
|---|---|---|
| amount required | string | Amount minted, as a decimal string. |
| toAddress required | string | Recipient address. |
| reserveBalance optional | string | Backing at time of mint. Kept on the event; the reserve ratio reads reserve.snapshot. |
| authorizedBy optional | string | Who authorized it, checked against the account’s approved-minters list. A txHash lets the reconciliation sweep match an onchain mint to this event. |
burn Approved minter
| Field | Type | Description |
|---|---|---|
| amount required | string | Amount burned. |
| fromAddress required | string | Address burned from. |
| authorizedBy optional | string | Authorizer — checked against the allowlist. |
transfer Large outflow · list match · allowlist
| Field | Type | Description |
|---|---|---|
| amount required | string | Value moved. |
| fromAddress required | string | Source address. |
| toAddress required | string | Destination address. |
| counterpartyType optional | string | Label for the counterparty, e.g. exchange, internal. |
payout Velocity · list match · allowlist
| Field | Type | Description |
|---|---|---|
| amount required | string | Payout amount. |
| toAddress required | string | Beneficiary address. |
| fromAddress optional | string | Paying wallet. |
| corridor optional | string | Payment corridor, e.g. US->NG. |
| customerId optional | string | The platform’s customer id. |
| beneficiaryInfo optional | object | Beneficiary details. Kept on the event; no policy reads corridor, customerId or beneficiaryInfo yet. |
redemption Recorded
| Field | Type | Description |
|---|---|---|
| status required | "requested" | "settled" | Redemption stage. |
| amount required | string | Amount redeemed. |
| holder required | string | Redeeming holder. |
| holderJurisdiction optional | string | Holder’s jurisdiction. |
| kycStatus optional | string | KYC status at redemption. |
A requested and a settled event that share a correlationId share one case reference. No policy judges a redemption yet.
reserve.snapshot Reserve ratio
| Field | Type | Description |
|---|---|---|
| reserveBalance required | string | Total reserves backing the asset. |
| circulatingSupply required | string | Tokens in circulation. The reserve-ratio policy compares it with reserveBalance on each snapshot. |
| custodian optional | string | Reserve custodian. |
| attestationRef optional | string | Attestation reference for evidence. |
reserve.movement Reserved: accepted and kept as sent; no policy reads it yet
| Field | Type | Description |
|---|---|---|
| amount required | string | Amount moved. |
| direction required | "in" | "out" | Into or out of reserve. |
| account required | string | Reserve account involved. |
| authorizedBy optional | string | Who authorized the movement. |
admin.action Privileged action
| Field | Type | Description |
|---|---|---|
| action required | enum | pause, unpause, blacklist, upgrade, roleGrant, roleRevoke, mintAuthChange. The privileged-action policy fires on pause, blacklist, upgrade, roleGrant and mintAuthChange; unpause and roleRevoke are recorded. |
| actor required | string | Who performed the action. |
| target optional | string | Address or role acted upon. |
kyc.updated Reserved: accepted and kept as sent; no policy reads it yet
| Field | Type | Description |
|---|---|---|
| subjectId required | string | The subject whose KYC changed. |
| status required | string | New KYC status. |
| expiresAt optional | string | When the status expires. |
Common fields
Every event accepts these alongside its own payload. At the SDK call site all are optional: the SDK fills occurredAt and idempotencyKey when absent. On the wire the server requires idempotencyKey on every envelope.
| Field | Type | Description |
|---|---|---|
| chain | string | One of the enabled slugs, in any letter case: arbitrum, avalanche, base, bitcoin, bnb, ethereum, optimism, polygon, solana. Any other value, a blank one or one that is not a string refuses the batch with 400; no chain passes. |
| asset | string | Asset symbol, e.g. USDX; up to 50 characters. |
| txHash | string | Onchain transaction hash; up to 100 characters. |
| correlationId | string | The platform’s internal id; pairs a redemption’s requested and settled events. |
| idempotencyKey | string | Dedupe key, up to 256 characters; the first write wins per account, source app and key. The SDK generates one per call when omitted. |
| fromPartyRef, toPartyRef | string | The account’s own id for the party behind each leg, the same customerRef the directory takes, under the customer-reference rule and checked exactly as sent. Empty or all-whitespace names no party; any other value outside the rule refuses the batch. |
| actor | string | Who or what triggered the event. |
| occurredAt | string | Date | When it happened; the SDK sends now when absent. More than an hour in the future is refused. |
| metadata | object | Extra context, kept on the event. Profile data such as names and contact details belongs in the directory, never in an event. |
identity.check.result is refused with 400 event_type_retired: “identity.check.result is no longer accepted. Verification steps are recorded by Hira’s hosted page.”
Wire format
The SDK builds this. A flush posts a batch of envelopes to the ingestion endpoint:
{
"events": [
{
"id": "3f1c…a9", // SDK-assigned uuid
"type": "mint",
"source": "sdk",
"schemaVersion": "1",
"occurredAt": "2026-07-02T04:15:22.000Z",
"idempotencyKey": "mint_8812",
"payload": { "amount": "250000.00", "asset": "USDX", "toAddress": "0x9f…", "authorizedBy": "minter-svc" }
}
]
}
The request carries Authorization: Bearer <key> and x-hira-sdk: ts/0.2.0. A successful ingest answers 202 Accepted with no body. A refused batch answers 400 field_invalid with a sentence naming the event, such as “events[0]: chain ‘eth’ is not a chain Hira knows. Known chains: …”.
POST /v1/check 200 · scope monitoring · 64 KiB
Asks before an action moves. The body is one event envelope, the shape a later /v1/events item of the same action would take. The answer is recorded: a breach raises a case as the executed action would, and a clean check is recorded as an allow. The proposed action is never stored as an event.
| Field | Type | Description |
|---|---|---|
| requestId | string | The call’s handle; a replay answers the original’s. |
| verdict | "Allow" | "Hold" | "Deny" | Proceed only on Allow. A policy band routed to In review answers Hold; one routed to Decline answers Deny. |
| declineType | "Retry" | "Final" | null | On a Deny: whether asking again can help. |
| reasonCode | "NoActivePolicy" | null | Set when no policy watched the event’s stream: a Deny with an empty decisions list. |
| expiresAt | string | null | On a Hold: when the open question leaves the review queue unanswered. Expiry is not a verdict. |
| degraded | boolean | A rule that applied could not be measured. |
| decisions | array | One per policy that held or declined: { policy, verdict, declineType, customerFacingReason }. |
The same idempotencyKey with the same body answers the recorded answer, which is how a caller polls a Hold; the same key with another body answers 400 field_invalid. identity.check.result answers 400 event_type_retired.
GET /v1/screening/address/{address} 200 · scope monitoring
Screens one address against the lists the account maintains. An 0x address of 40 hex digits in any casing is lower-cased; any other, such as base58, is kept exactly. Each answered call is billable; refusals are not.
| Field | Type | Description |
|---|---|---|
| identifier | string | The address in canonical form; match on it. |
| listed | boolean | On a list this account maintains, in force now. |
| flags | array | The matching entries, at most 25: { source, firstParty, listedAt }. Closed entries are never returned. |
| monitored | boolean | The account already watches this address as a wallet. |
| externalScreeningConsulted | boolean | false today, always: no external sanctions or watchlist source is connected, so listed: false means only “not on a list this account maintains”. |
| disclaimer | string | The same fact as a sentence. |
| screenedAt | string | When the answer was produced, ISO 8601. |
GET /v1/exceptions · GET /v1/approvals 200 · scope monitoring
GET /v1/exceptions answers the latest cases raised by the programs subscribed to the key’s source app, newest first, at most 50: { total, totalCapped, hard, items }, each item { id, outcomeType, title, triggerReason, sourceType, sourceEventReference, triggeredAt } with outcomeType HardBreach or SoftBreach. A full page sets totalCapped: true, total is then a floor and hard is null. The SDK has no method for it.
GET /v1/approvals answers the decisions recorded on the key’s streams, at most 50: { total, totalCapped, pending, items }. ?status= narrows to pending, approved, declined, superseded, withdrawn or expired; any other word answers 400 field_invalid. Each item is { id, status, declineType, reason, reference, exceptionId, origin, requestedAt, decidedAt, expiresAt }, with declineType retry or final and origin detective or preventive. pending is stated only when the page holds every waiting decision, else null. GET /v1/approvals/{id} answers one item, and 404 for an id outside the key’s scope. An account without the Approvals API answers 403 product_not_enabled. In the SDK: hira.approvals.list({ status }) and hira.approvals.get(id).
POST /v1/reset clears a sandbox source app’s stream data (its events, the exceptions they raised and wallets nothing else holds) and answers { exceptions, events, wallets }. It runs in Development and Demo environments alone; elsewhere, or for an app that is not a sandbox app, it answers 404.
Client surface
new Hira(config) creates a client. It throws without an apiKey, on an endpoint that is not https (plain http on localhost, 127.0.0.1 and [::1] alone) or that carries a user name or password, and on a hira_sk_ key where a DOM or a web worker is present. Construct one per process and reuse it.
hira.verificationSessions: create(input, { idempotencyKey?, signal? }), get(id, { signal? }), list({ customerRef?, clientReference?, programId?, createdAfter?, limit?, startingAfter?, endingBefore?, signal? }), cancel(id, { signal? }), createLink(id, { expiresIn?, signal? }) and simulate(id, { outcome }, { signal? }). Each throws HiraError on a refusal, with the server’s code.
hira.directory.users.create(input) and hira.directory.businesses.create(input) answer the receipt and throw HiraDirectoryError.
constructEvent(rawBody, header, secret, { tolerance?, now? }), exported on its own and as hira.webhooks.constructEvent, verifies a webhook delivery with no API key and throws HiraSignatureError.
hira.check(type, payload, { signal? }) answers the check and a poll() that asks again under the same key; it throws HiraCheckError. hira.screen(address, { signal? }) answers the screening and throws HiraScreeningError. hira.approvals.list() and .get(id) throw HiraApprovalsError. Every error class extends HiraError, which carries status, code, param, retryable and requestId.
hira.track(type, payload): void enqueues an event, never blocks and never throws; payload is typed to type. The background timer starts with the first track(). hira.flush() sends what is buffered; with the default failOpen: true it resolves even when a batch was dropped. hira.shutdown() flushes, then stops the timer.
Each awaited call takes a signal: an abort or AbortSignal.timeout(ms) ends the call with the caller’s own error, never retried and never a HiraError.