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.

ModeWhere shared values go
DirectKept by LutraID for a limited time. Read them with the verification result call.
IntermediarySent 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.

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.

  1. Enter the Address of your system: a public https:// URL that accepts POST with JSON. Private, loopback and reserved addresses are refused, and an installation can limit which addresses are allowed.
  2. Save endpoint, then copy the Signing secret. It is shown once.
  3. 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 }
    }
  ]
}
FieldMeaning
verification_idThe check's ID, the same as in the API, webhooks and Records
customer_rp_identifier, customer_service_identifierYour registered relying party and service that the check ran for
statussuccess or failure
error_codeOnly when the wallet returned an error, for example access_denied when the person declined
claimsOne 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:

HeaderValue
X-Lutraid-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Idempotency-KeyThe verification_id, the same on every retry
X-Lutraid-Organization-IdYour organization ID
Content-Typeapplication/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 2xx within three seconds. Store the values first, then respond.
  • Network errors, timeouts, 429 and 5xx are 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.

Was this page helpful?