Receive check results
In Intermediary mode, LutraID runs the wallet interaction for you and sends each check result, including the shared values, to your own system. LutraID keeps no values, so this endpoint is the only place you receive them. Every verifier needs one before its checks can send results.
Which mode applies to you
The mode is set for the whole installation, not per verifier. Self-hosted installations always run Direct. The hosted service runs Intermediary mode.
| Mode | Where shared values go |
|---|---|
| Direct | Kept by LutraID for a limited time. Read them with the verification result call. |
| Intermediary | Sent to your result endpoint, then discarded. The API returns the outcome only. |
To see which applies, open a verifier. In Intermediary mode it has a Result delivery tab; in Direct mode there is nothing to set up.
Intermediary results currently flow in Sandbox only. Production registration in Intermediary mode isn't available on LutraID yet; see Production setup.
Set up the result endpoint
Owners and admins set it on the verifier's Result delivery tab. Each verifier has its own endpoint and its own signing secret.
- Enter the Address of your system: a public
https://URL that acceptsPOSTwith JSON. Private, loopback and reserved addresses are refused, and an installation can limit which addresses are allowed. - Save endpoint, then copy the Signing secret. It is shown once.
- Send test result to check that your system receives it and accepts the signature.
To try the flow before your own system is ready, choose Use LutraID test inbox. The test inbox shows what your system would receive, including the shared values, checks the signature, and deletes each message after one hour. Switching from the test inbox to your own endpoint issues a new secret.
Rotate secret… issues a new secret. The current one stops working at once, so every result is rejected by your system until you install the new one.
Payload
LutraID sends one POST per finished check.
Check result
{
"version": 1,
"verification_id": "6d2f0b8a-1c4e-4a9b-8f73-2e5d9c1a0b47",
"customer_rp_identifier": "…",
"customer_service_identifier": "…",
"status": "success",
"claims": [
{
"credential_query_id": "…",
"validated_claims": { "age_over_18": true }
}
]
}
| Field | Meaning |
|---|---|
verification_id | The check's ID, the same as in the API, webhooks and Records |
customer_rp_identifier, customer_service_identifier | Your registered relying party and service that the check ran for |
status | success or failure |
error_code | Only when the wallet returned an error, for example access_denied when the person declined |
claims | One entry per requested document, with the values that passed validation. Empty on failure |
A test result has "test": true, "status": "test" and empty claims. Expired checks, where no wallet answered, are not sent.
Verify the signature
Every request carries these headers:
| Header | Value |
|---|---|
X-Lutraid-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Idempotency-Key | The verification_id, the same on every retry |
X-Lutraid-Organization-Id | Your organization ID |
Content-Type | application/json |
The signature works like the webhook signature, with the verifier's result signing secret: compute HMAC-SHA256 over <t>.<raw request body>, compare it with v1 in constant time, and reject the request if t is more than five minutes from your clock. Use the raw body bytes, not re-serialized JSON.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyResult(secret, header, rawBody) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '')
if (!match) return false
const [, t, v1] = match
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest()
return timingSafeEqual(expected, Buffer.from(v1, 'hex'))
}
Delivery and retries
- Answer with any
2xxwithin three seconds. Store the values first, then respond. - Network errors, timeouts,
429and5xxare retried twice, after one and three seconds. Any other status, including a redirect, fails at once. - If delivery fails, the check record says Result could not be sent to your system. The values are gone and can't be sent again; the outcome and reference stay on the record. The person who shared still sees the normal completion page.
- The Result delivery tab lists the last 20 deliveries with time, HTTP status and reference. Deduplicate by
Idempotency-Key.
What the API returns in this mode
The verification result call still returns the outcome, but sharedClaims is null. Its resultDelivery field says whether the result reached your system: delivered, failed or pending, with the time of the latest attempt. In Direct mode resultDelivery is null.
Webhooks are optional in both modes. They tell you that a check completed and carry IDs only; they never replace the result endpoint.