Webhooks

Webhooks let Lira push real-time notifications to your server when a verification completes or fails — eliminating the need to poll the API and enabling event-driven workflows like KYC approvals, user onboarding triggers, and payment releases.

When a verification finishes processing (in async mode), Lira sends a signed POST request to your registered HTTPS endpoint with the full result. Your server verifies the signature and processes the event.


Before you start

Webhook management uses a JWT Bearer token in the Authorization: Bearer header. See Authentication if you need to obtain or refresh one.

Required roles

Action Minimum role
Create, list, view, update a webhook ORG_ADMIN or DEVELOPER
Delete a webhook ORG_ADMIN

For the full async verification flow — including how to submit async verifications and handle webhook events — see Async Verification with Webhooks.


Events

Event When it fires
verification.completed A verification was successfully processed (status: success)
verification.failed A verification attempt failed (status: failed or error)

Create a webhook

Register an HTTPS endpoint to start receiving events.

curl -X POST https://api.lira.com/api/v1/client/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/lira",
    "events": ["verification.completed", "verification.failed"]
  }'

Request body

Field Type Required Description
url string Yes HTTPS URL Lira will POST events to. Must be publicly reachable.
events string[] Yes One or more event types to subscribe to.
secret string No Signing secret (minimum 16 characters). Auto-generated if omitted.

URL requirements

  • Must use https://
  • Must be publicly reachable from the internet
  • localhost and private IP ranges are not permitted

Response 201 Created

{
  "id": "WEBHOOK_ID",
  "organizationId": "org_...",
  "url": "https://yourapp.com/webhooks/lira",
  "events": ["verification.completed", "verification.failed"],
  "status": "active",
  "createdAt": "2026-03-09T10:00:00.000Z",
  "updatedAt": "2026-03-09T10:00:00.000Z"
}

Important: The webhook signing secret is not returned in the response. If you provided your own secret, it is stored and will be used to sign deliveries. If you omitted it, Lira auto-generated one — retrieve it from your organisation's secrets configuration immediately. You cannot recover it later.


Verifying signatures

Every request Lira sends to your endpoint includes an X-Signature header. Verify it before processing the event to confirm the request came from Lira and the payload has not been tampered with.

Header format

X-Signature: sha256=<hex-signature>

The signature is computed as HMAC-SHA256 over the raw request body using your webhook secret. Always use the raw body — not a parsed JSON object — for signature verification. Parsing and re-serialising JSON can alter whitespace and change the computed hash.

Warning: Always verify signatures before processing any webhook payload. Use constant-time comparison functions (timingSafeEqual in Node.js, compare_digest in Python) to prevent timing attacks. Reject invalid signatures with 401.

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  const signature = signatureHeader.replace('sha256=', '');
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const sigBuf = Buffer.from(signature);
  const expectedBuf = Buffer.from(expected);

  if (sigBuf.length !== expectedBuf.length) return false;
  return crypto.timingSafeEqual(sigBuf, expectedBuf);
}

// Express handler — use raw body parser so the signature matches
app.post('/webhooks/lira', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'];

  if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // handle event...
  res.status(200).send('ok');
});

Python

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    signature = signature_header.replace('sha256=', '')
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# Flask handler
@app.route('/webhooks/lira', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Signature', '')

    if not verify_webhook_signature(request.get_data(), signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.json
    # handle event...
    return 'ok', 200

Handling events

Lira expects a 2xx response from your endpoint. Any other status code or a connection timeout is treated as a failed delivery and triggers a retry.

Respond quickly. Return 200 OK immediately and process the event asynchronously in a background job. Long-running handlers cause timeouts and trigger retries.

Be idempotent. Lira may deliver the same event more than once when retrying. Store the delivery id and skip processing if you have already handled it.

Event payload

{
  "event": "verification.completed",
  "verificationId": "ver_...",
  "organizationId": "org_...",
  "timestamp": "2026-03-09T10:05:00.000Z",
  "data": {}
}
Field Description
event Event type: verification.completed or verification.failed
verificationId The ID of the verification this event relates to
organizationId Your organisation ID
timestamp ISO 8601 timestamp of when the event was emitted
data The full verification result. Shape matches the GET /verify/:id response.

Retries

If your endpoint does not return a 2xx response, Lira retries with exponential backoff:

Attempt Delay
1 Immediate
2 1 minute
3 3 minutes
4 9 minutes
5 27 minutes

After 5 failed attempts, the webhook status is automatically set to failed and no further deliveries are made. Re-enable it using Update a webhook.


Update a webhook

Update the URL, subscribed events, status, or signing secret.

curl -X PATCH https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "active",
    "events": ["verification.completed"]
  }'

Updatable fields

Field Type Description
url string New HTTPS endpoint URL
events string[] Updated list of subscribed events
status active | inactive | failed Enable, disable, or re-enable the webhook
secret string Rotate the signing secret (minimum 16 characters)

Get a webhook

curl https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

List webhooks

curl "https://api.lira.com/api/v1/client/webhooks" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Filter by status or event type:

curl "https://api.lira.com/api/v1/client/webhooks?status=failed" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

curl "https://api.lira.com/api/v1/client/webhooks?event=verification.completed" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

View delivery history

List recent deliveries for a webhook:

curl "https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Filter for failed deliveries:

curl "https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries?status=failed" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Get a specific delivery by ID:

curl "https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries/DELIVERY_ID" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Delivery object

{
  "id": "del_...",
  "webhookId": "WEBHOOK_ID",
  "verificationId": "ver_...",
  "status": "failed",
  "attempts": 3,
  "lastAttemptAt": "2026-03-09T10:20:00.000Z",
  "payload": {},
  "response": {},
  "createdAt": "2026-03-09T10:05:00.000Z",
  "updatedAt": "2026-03-09T10:20:00.000Z"
}
Delivery status Meaning
pending Delivery is queued or in progress
success Your endpoint returned a 2xx response
failed All retry attempts exhausted without a 2xx response

Delete a webhook

curl -X DELETE https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response 204 No Content

Note: Deleting a webhook immediately stops all future deliveries. Pending deliveries that are in a retry queue may still be attempted. If you want to pause deliveries temporarily, update status to inactive instead.


Troubleshooting

Deliveries are not arriving

  1. Check that your webhook URL uses https:// and is publicly reachable from the internet. Verify it is not behind a VPN or firewall that blocks inbound requests.
  2. Check the webhook status — GET /client/webhooks/WEBHOOK_ID. If status is failed, all retry attempts were exhausted. Update status to active to resume.
  3. Inspect recent deliveries for error details — GET /client/webhooks/WEBHOOK_ID/deliveries?status=failed. The response field shows what your endpoint returned.
  4. Confirm your endpoint returns a 2xx status code. 3xx redirects are not followed by Lira.

Signature verification is failing

  1. Ensure you are computing the HMAC over the raw request body bytes, not a parsed or re-serialised JSON object.
  2. Confirm the WEBHOOK_SECRET environment variable contains the same secret that was used when creating the webhook.
  3. Use the exact string sha256= prefix when comparing — do not strip it before computing.

Webhook status became failed

Re-enable the webhook:

curl -X PATCH https://api.lira.com/api/v1/client/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": "active" }'

Once re-enabled, Lira will deliver events for any new verifications that complete. Events from the period when the webhook was failed are not retroactively delivered. Use GET /verify or GET /verify/VERIFICATION_ID to retrieve results for any verifications that completed during the outage.


Next steps