Overview
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
Webhooks are configured in the Lira dashboard — you register endpoints, rotate secrets, and inspect delivery history there. Your application's only job is to receive the signed events and verify them, which is what the rest of this guide covers.
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) |
Register a webhook
Register your endpoint from the dashboard:
- Sign in at app.uselira.com and open Webhooks.
- Add an endpoint. Enter the HTTPS URL Lira should POST events to, and select the events to subscribe to (
verification.completed,verification.failed). - Copy the signing secret. The dashboard shows the webhook's signing secret once, when the endpoint is created. Copy it immediately and store it as an environment variable (e.g.
WEBHOOK_SECRET) — you'll need it to verify signatures, and it cannot be recovered later.
URL requirements
- Must use
https:// - Must be publicly reachable from the internet
localhostand private IP ranges are not permitted
Note
During local development, expose your local server with a tunnel such as ngrok or Cloudflare Tunnel so Lira can reach it over HTTPS.
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', 200Handling 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": "...",
"organizationId": "...",
"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 organization 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 from the Webhooks page in the dashboard.
Managing webhooks
Manage your endpoints from the Webhooks page in the dashboard:
- Edit an endpoint's URL, subscribed events, or status (
active/inactive). - Rotate the signing secret. After rotating, update
WEBHOOK_SECRETin your application before the old secret stops being used. - Disable or delete an endpoint. Disabling pauses deliveries without removing the endpoint; deleting stops all future deliveries permanently.
Delivery history is available both in the dashboard and via the API — see Delivery history below.
Delivery history
Inspect delivery attempts for a webhook programmatically with your API key. You'll need the webhook's ID, shown on the Webhooks page in the dashboard.
List deliveries for a webhook
curl "https://api.uselira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries" \
-H "X-API-Key: YOUR_API_KEY"Filter by status (pending, success, failed):
curl "https://api.uselira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries?status=failed" \
-H "X-API-Key: YOUR_API_KEY"Get a single delivery
curl "https://api.uselira.com/api/v1/client/webhooks/WEBHOOK_ID/deliveries/DELIVERY_ID" \
-H "X-API-Key: YOUR_API_KEY"Delivery object
{
"id": "del_...",
"webhookId": "WEBHOOK_ID",
"verificationId": "...",
"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 |
Troubleshooting
Deliveries are not arriving
- 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. - Check the endpoint status on the Webhooks page. If it is
failed, all retry attempts were exhausted — set it back toactiveto resume. - Inspect recent deliveries — in the dashboard or via
GET /client/webhooks/WEBHOOK_ID/deliveries?status=failed(see Delivery history). The response your endpoint returned is shown for each failed delivery. - Confirm your endpoint returns a
2xxstatus code.3xxredirects are not followed by Lira.
Signature verification is failing
- Ensure you are computing the HMAC over the raw request body bytes, not a parsed or re-serialised JSON object.
- Confirm the
WEBHOOK_SECRETenvironment variable contains the same secret shown in the dashboard for this endpoint. - Use the exact string
sha256=prefix when comparing; do not strip it before computing.
Webhook status became failed
Re-enable the endpoint from the Webhooks page in the dashboard. Once re-enabled, Lira delivers 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.