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
localhostand 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 (
timingSafeEqualin Node.js,compare_digestin Python) to prevent timing attacks. Reject invalid signatures with401.
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
statustoinactiveinstead.
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 webhook status —
GET /client/webhooks/WEBHOOK_ID. Ifstatusisfailed, all retry attempts were exhausted. Update status toactiveto resume. - Inspect recent deliveries for error details —
GET /client/webhooks/WEBHOOK_ID/deliveries?status=failed. Theresponsefield shows what your endpoint returned. - 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 that was used when creating the webhook. - 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
- Async Verification with Webhooks — end-to-end walkthrough of the full async verification and webhook delivery flow
- Go Live — webhook security checklist for production
- Errors — complete error reference