What are webhooks?
Webhooks let your servers receive real-time notifications when events happen in Lymnus. Instead of polling our API, Lymnus sends an HTTP POST to your URL the moment an event occurs.
Enterprise teams can configure webhook endpoints through their platform administrator.
Supported events:
extraction.completed: A document extraction finishes successfully
extraction.failed: An extraction fails permanently
processing.completed: Data processing finishes successfully
generation.completed: Synthetic data generation finishes
report.generated: A report, financial model, or an audit is ready
agent.run_completed: A workflow agent run finishes
agent.run_failed: A workflow agent run fails
team.member_added: A new member joins your team
team.member_removed: A member is removed from your team
subscription.created: A subscription is created
subscription.canceled: A subscription is canceled
Payload format:
Every webhook delivery uses this envelope:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "extraction.completed",
"created": "2025-04-06T14:32:00Z",
"livemode": true,
"api_version": "v1",
"data": {
"project_id": 42,
"project_slug": "my-project-abc123",
"project_name": "Q1 Invoices",
"format": "json",
"total_records": 847
}
}Verifying webhook signatures:
Every delivery includes an X-Lymnus-Signature header. Always verify this signature before processing the payload — it proves the request came from Lymnus and not a third party.
Signature format:
X-Lymnus-Signature: t=1712413920,v1=a4b3c2d1e5f6...Verification code — PHP:
function verifyLymnusWebhook(
string $payload,
string $signature,
string $secret
): bool {
$parts = [];
foreach (explode(',', $signature) as $part) {
[$k, $v] = explode('=', $part, 2);
$parts[$k] = $v;
}
$timestamp = $parts['t'] ?? null;
$v1 = $parts['v1'] ?? null;
if (!$timestamp || !$v1) return false;
// Reject replays older than 5 minutes
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $v1);
}
// Usage in your webhook handler:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_LYMNUS_SIGNATURE'] ?? '';
$secret = 'whsec_your_signing_secret_here';
if (!verifyLymnusWebhook($payload, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
// Handle $event['event'] ...Verification code — Node.js:
const crypto = require('crypto');
function verifyLymnusWebhook(payload, signature, secret) {
const parts = Object.fromEntries(
signature.split(',').map(p => p.split('='))
);
const timestamp = parts['t'];
const v1 = parts['v1'];
if (!timestamp || !v1) return false;
// Reject replays older than 5 minutes
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(v1)
);
}Verification code — Python:
import hmac
import hashlib
import time
def verify_lymnus_webhook(payload: bytes, signature: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature.split(','))
timestamp = parts.get('t')
v1 = parts.get('v1')
if not timestamp or not v1:
return False
# Reject replays older than 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, v1)Retry behaviour:
If your endpoint does not return a 2xx status code within 10 seconds, Lymnus retries the delivery with exponential backoff:
1st retry: 1 minute
2nd retry: 5 minutes
3rd retry: 30 minutes
4th retry: 2 hours
5th retry (final): 8 hours
After 5 failed attempts the delivery is marked exhausted and no further retries occur. You can manually trigger a retry from your webhook delivery log.
Best practices:
Respond quickly — return HTTP 200 immediately and process the event asynchronously in a background job.
Be idempotent — the same event may be delivered more than once in rare cases. Use id (the delivery UUID) to deduplicate.
Always verify signatures — never process a payload without verifying the
X-Lymnus-Signatureheader.Handle all events — your handler should return 200 even for events you do not care about. Returning 4xx causes unnecessary retries.