Webhooks
Receive real-time events from Genlytic via signed HTTP POST.
Genlytic delivers webhook events as signed HTTP POST requests. Configure endpoints in Settings → API → Webhooks.
Available events
| Event type | Fired when |
|---|---|
scan.completed | A scan batch finishes processing |
visibility.drop | Visibility score drops 5+ points vs previous scan |
visibility.spike | Visibility score rises 5+ points vs previous scan |
citation.lost | A previously cited domain is no longer cited |
citation.gained | A new domain is cited for the first time |
action.generated | New optimize actions are created after a scan |
agent.action.completed | A GEO Agent coordinator cycle dispatches tasks |
agent.run.completed | A GEO Agent coordinator run finishes |
shadow_competitor.detected | A shadow competitor crosses the detection threshold |
Payload structure
All webhook payloads use this envelope:
{
"id": "evt_a1b2c3d4e5f6...",
"type": "scan.completed",
"created": 1735689600,
"data": {}
}
The data field is event-specific.
Signature verification
Every delivery includes three headers:
| Header | Value |
|---|---|
X-Genlytic-Event | Event type string |
X-Genlytic-Timestamp | Unix timestamp (seconds) |
X-Genlytic-Signature | sha256=<hmac_hex> |
The HMAC-SHA256 signature is computed over ${timestamp}.${rawBody} using your webhook secret (the whsec_xxx value shown when you created the endpoint) as the key directly.
import crypto from 'crypto';
function verifyWebhook(
rawBody: string,
headers: Record<string, string>,
webhookSecret: string, // the whsec_xxx value from Settings → API → Webhooks
): boolean {
const timestamp = headers['x-genlytic-timestamp'];
const received = headers['x-genlytic-signature']?.replace('sha256=', '');
if (!timestamp || !received) return false;
// Reject stale events (replay attack prevention)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) return false; // 5 minutes
const expected = crypto
.createHmac('sha256', Buffer.from(webhookSecret, 'utf8'))
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Constant-time comparison
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(received.padEnd(expected.length, '0'), 'hex');
return a.byteLength === b.byteLength && crypto.timingSafeEqual(a, b);
}
Reject requests where the timestamp is more than 5 minutes old. The example above includes this check.
Always verify the signature before processing the payload. Never trust unverified webhook bodies.
Retry policy
Genlytic makes one delivery attempt with a 10-second timeout. Failed deliveries (non-2xx or timeout) are not retried. Return 2xx immediately and process the event asynchronously.
Testing
Use the Send test event button in Settings → API → Webhooks to fire a test payload to your endpoint. Test events are signed identically to live events.
Next steps
- Authentication — API key setup
- Rate Limits — limits by plan