Search the docs or ask a question /
Reference

API reference

Every /v1 door, its refusals, and the webhook envelope

Get started with the API

POST/v1/verification-sessions201
programIdcustomerRefclientReferenceexpectedDetailsapplicant expiresInmetadatasimulateIdempotency-Key

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:

request 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.

ScopeConsole labelDoors
verificationIdentity verification/v1/verification-sessions and every door below it.
directoryDirectory/v1/directory/users, /v1/directory/businesses.
monitoringTransaction 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.

400 Bad Request
{
  "error": "A link lasts between 10 minutes and 7 days.",
  "code": "expires_in_out_of_range",
  "param": "expiresIn"
}
CodeStatusWhen
request_invalid400The body is not a JSON object, or arrives under another media type or none. “The request body must be a JSON object.”
unknown_field400The body names a field the door does not declare, nested ones included; param names it.
field_required400A required field is missing.
field_invalid400A field is not in the shape the door takes.
customer_ref_invalid400A 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_data400A session create or list reference contains @ or starts with + (the directory and events doors answer field_invalid).
metadata_invalid400Metadata past its limits, holding a control character, or holding a value that looks like an email address or a phone number.
expected_details_invalid400Expected details hold a control character, or the details on record cannot be compared against.
expected_details_required400The flow screens the person or the company and neither the request nor the party record holds what it needs.
return_url_invalid400returnUrl is not an absolute https URL of at most 2048 characters with no userinfo.
return_url_origin_not_allowed400Any well-formed returnUrl: no account holds an allowed origin today.
expires_in_out_of_range400expiresIn outside 600 to 604800 seconds.
simulate_not_allowed400Simulate on a live account.
idempotency_key_invalid400Idempotency-Key is not 1 to 255 printable ASCII characters.
filters_not_combinable400A list names more than one filter, or both cursors.
event_type_retired400identity.check.result sent to /v1/events or /v1/check.
api_key_invalid401The key is missing or not valid.
api_key_scope_missing403The key does not reach the door’s product.
product_not_enabled403The account’s plan does not include the product.
account_suspended403The account is suspended or not active.
engine_not_connected403A live key creating a verification session.
session_not_found404No session of the key’s account has that id. An id in another account answers this too, never 403.
program_not_found404No verification program of the account has that id, the program subscribes to streams, or it is archived.
program_not_active409The program is paused, its flow cannot be read, or a step of its flow has no policy that judges it.
party_kind_mismatch409The customerRef names a party of the other kind.
party_unavailable409The party is blocked or was merged into another record.
session_state_invalid409The session’s state does not allow the verb.
daily_quota_reached409A sandbox account made 500 verification sessions in the UTC day.
request_too_large413The body is larger than the route accepts. “The request body is larger than this route accepts.”
idempotency_key_reused422An Idempotency-Key already used with another body.
rate_limited429A window is spent; Retry-After says when to retry.
internal_error500“An unexpected error occurred.”
service_unavailable503A 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.

WindowPer minuteDoors
create, per key300Session create, cancel, links and simulate; the directory creates.
read, per key600Session get and list; screening, exceptions, approvals.
ingest, per key300/v1/events, /v1/check, /v1/reset.
account, all keys1,200Every key-authed door.
invalid key, per address30Requests 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 201 with the original session, no url, and the header Idempotent-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 422 idempotency_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.

FieldTypeDescription
idstringvsn_ and 32 hexadecimal characters.
object"verification_session"The object’s name, the same word a webhook’s data.object carries.
livemodebooleanfalse on a sandbox account.
statusstringcreated, 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.
decisionstring | nullapproved, declined, in_review; null while the session is open.
assurancestring | nullmeasured when a source checked the person, simulated when the outcome was asked for in the sandbox.
seqintegerThe 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.
originstringapi (this door), link (a program’s shared link) or operator (started in the console).
programIdstringThe program whose flow the person walks.
flowRevisionintegerThe revision of the program’s flow the session walks.
customerRefstring | nullThe account’s own identifier for the party.
clientReferencestring | nullThe account’s own reference for this session.
partyIdstring | nullThe directory party the session names.
returnUrlstring | nullNull today; see return_url_origin_not_allowed.
metadataobject | nullThe string pairs sent at create.
redactionobject | null{ "status": "completed", "redactedAt" } once the applicant’s data was erased in the console.
createdAt, startedAt, completedAt, expiresAt, canceledAtstring | nullISO 8601 instants in UTC; createdAt and expiresAt are always set.
urlstringThe applicant link, on a fresh create and on /links alone; absent everywhere else.

GET /v1/verification-sessions/{id} adds:

FieldTypeDescription
decisionDetailobject | nulldeclineType (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 }.
errorDetailobject | null{ code }: why a session stands in error.
expectedDetailsSuppliedbooleanWhether the session holds expected details, sent at create or taken from the party record. The details themselves never come back.
applicantobject | null{ country, subdivision } as the create stated them.
stepsarrayEach concluded step’s latest attempt: stepKey, outcome (verified, failed, unable-to-verify), assurance, attempt, concludedAt, notChecked ([{ measure, cause }]). No signal and no score.
pendingStepsstring[]The step keys still due.
casesarrayCases raised on the walk: { id, status, decisionIds }, the status in lower case.
consentobject | null{ faceStep, decidedAt }: the latest biometric consent decision (granted, refused, withdrawn). Never the notice’s text.
riskRatingstring | nulllow, 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.

FieldTypeDescription
programId requiredstringA hosted, active program of the account, in any of its workspaces. The session takes the program’s workspace and its current flow revision.
customerRef requiredstringThe 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 optionalstringThe account’s own reference for this session, under the same rule.
expectedDetails optionalobjectWhat 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 optionalobjectcountry (ISO 3166-1) and subdivision (ISO 3166-2): where the applicant is.
expiresIn optionalintegerSeconds the link lasts, 600 to 604800; 259200 (72 hours) unset.
metadata optionalobjectAt 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 optionalobjectSandbox only: { "outcome": "verified" | "failed" | "unable-to-verify" }. The hosted page opens each step on that outcome.
returnUrl optionalstringRefused 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.

QueryTypeDescription
customerRefstringSessions of one party reference.
clientReferencestringSessions under one session reference.
programIdstringSessions of one program.
createdAfterstringAn ISO 8601 date or instant; without an offset it reads as UTC.
limitinteger1 to 100; 25 unset.
startingAfterstringA session id from a page: the page after it.
endingBeforestringA 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.

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 typeSent when
verification_session.decidedA decision is set or changes; an in_review session later approved sends a second one.
verification_session.reopenedA 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.expiredA session that never started passes its deadline.
verification_session.abandonedA started session passes its deadline.
verification_session.canceledA session is canceled.

A session moving to error sends no event. The body is the same bytes on every attempt and resend:

FieldTypeDescription
idstringevt_…; evt_test_… on a test send. Dedupe on it.
object"event"
typestringOne of the five types above.
apiVersionstring2026-10-01.
createdAtstringWhen the change was recorded, ISO 8601 in UTC with microseconds.
livemodebooleanfalse for a sandbox account’s events.
dataobject{ 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:

request header
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

FieldTypeDescription
displayName requiredstring1 to 200 characters, trimmed.
customerRef optionalstringThe account’s own id for the person, under the customer-reference rule; the idempotency key. Blank reads as none.
status optionalstringActive (unset), InReview, Flagged or Blocked. A status already decided in the console on that reference stands.
dateOfBirth optionalstringYYYY-MM-DD.
email, phone optionalstringAn email address up to 320 characters; a phone number up to 40.
address optionalobjectline1, line2 (200), city, state, country (100), postalCode (20). Strict too.

POST /v1/directory/businesses 200

FieldTypeDescription
legalName requiredstring1 to 200 characters, trimmed.
customerRef optionalstringAs for users.
status optionalstringAs for users.
registrationNumber optionalstringUp to 60 characters.
incorporationCountry optionalstringUp 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

FieldTypeDescription
amount requiredstringAmount minted, as a decimal string.
toAddress requiredstringRecipient address.
reserveBalance optionalstringBacking at time of mint. Kept on the event; the reserve ratio reads reserve.snapshot.
authorizedBy optionalstringWho 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

FieldTypeDescription
amount requiredstringAmount burned.
fromAddress requiredstringAddress burned from.
authorizedBy optionalstringAuthorizer — checked against the allowlist.

transfer Large outflow · list match · allowlist

FieldTypeDescription
amount requiredstringValue moved.
fromAddress requiredstringSource address.
toAddress requiredstringDestination address.
counterpartyType optionalstringLabel for the counterparty, e.g. exchange, internal.

payout Velocity · list match · allowlist

FieldTypeDescription
amount requiredstringPayout amount.
toAddress requiredstringBeneficiary address.
fromAddress optionalstringPaying wallet.
corridor optionalstringPayment corridor, e.g. US->NG.
customerId optionalstringThe platform’s customer id.
beneficiaryInfo optionalobjectBeneficiary details. Kept on the event; no policy reads corridor, customerId or beneficiaryInfo yet.

redemption Recorded

FieldTypeDescription
status required"requested" | "settled"Redemption stage.
amount requiredstringAmount redeemed.
holder requiredstringRedeeming holder.
holderJurisdiction optionalstringHolder’s jurisdiction.
kycStatus optionalstringKYC 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

FieldTypeDescription
reserveBalance requiredstringTotal reserves backing the asset.
circulatingSupply requiredstringTokens in circulation. The reserve-ratio policy compares it with reserveBalance on each snapshot.
custodian optionalstringReserve custodian.
attestationRef optionalstringAttestation reference for evidence.

reserve.movement Reserved: accepted and kept as sent; no policy reads it yet

FieldTypeDescription
amount requiredstringAmount moved.
direction required"in" | "out"Into or out of reserve.
account requiredstringReserve account involved.
authorizedBy optionalstringWho authorized the movement.

admin.action Privileged action

FieldTypeDescription
action requiredenumpause, unpause, blacklist, upgrade, roleGrant, roleRevoke, mintAuthChange. The privileged-action policy fires on pause, blacklist, upgrade, roleGrant and mintAuthChange; unpause and roleRevoke are recorded.
actor requiredstringWho performed the action.
target optionalstringAddress or role acted upon.

kyc.updated Reserved: accepted and kept as sent; no policy reads it yet

FieldTypeDescription
subjectId requiredstringThe subject whose KYC changed.
status requiredstringNew KYC status.
expiresAt optionalstringWhen 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.

FieldTypeDescription
chainstringOne 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.
assetstringAsset symbol, e.g. USDX; up to 50 characters.
txHashstringOnchain transaction hash; up to 100 characters.
correlationIdstringThe platform’s internal id; pairs a redemption’s requested and settled events.
idempotencyKeystringDedupe key, up to 256 characters; the first write wins per account, source app and key. The SDK generates one per call when omitted.
fromPartyRef, toPartyRefstringThe 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.
actorstringWho or what triggered the event.
occurredAtstring | DateWhen it happened; the SDK sends now when absent. More than an hour in the future is refused.
metadataobjectExtra 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:

POST {endpoint}/v1/events
{
  "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.

FieldTypeDescription
requestIdstringThe 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" | nullOn a Deny: whether asking again can help.
reasonCode"NoActivePolicy" | nullSet when no policy watched the event’s stream: a Deny with an empty decisions list.
expiresAtstring | nullOn a Hold: when the open question leaves the review queue unanswered. Expiry is not a verdict.
degradedbooleanA rule that applied could not be measured.
decisionsarrayOne 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.

FieldTypeDescription
identifierstringThe address in canonical form; match on it.
listedbooleanOn a list this account maintains, in force now.
flagsarrayThe matching entries, at most 25: { source, firstParty, listedAt }. Closed entries are never returned.
monitoredbooleanThe account already watches this address as a wallet.
externalScreeningConsultedbooleanfalse today, always: no external sanctions or watchlist source is connected, so listed: false means only “not on a list this account maintains”.
disclaimerstringThe same fact as a sentence.
screenedAtstringWhen 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.

Mapping a product to the right events and policies? Book a demo — we’ll help scope it.