A webhook sends events from Overlane to an https endpoint you run. Events are signed, ordered per webhook, retried when your endpoint is down, and safe to receive twice.
Register one
overlane webhook create --url https://hooks.example.com/overlane --event approval.requested --event task.updated
overlane webhook test whk_0123456789abcdef
overlane webhook deliveries whk_0123456789abcdefThe URL must be https on the default port (or 8443) and a public DNS name: an IP address, a local or internal name, or a URL with credentials is refused. The signing secret starts with whs_ and is shown once, at creation and at rotation; it is derived, never stored. overlane webhook rotate bumps the key version and the old secret stops verifying new events at once. Creating, rotating or disabling a webhook is in the bots autonomy class.
Events
| Event | Sent when |
|---|---|
task.created |
A task is created |
task.updated |
A task is assigned or an operation runs on it |
approval.requested |
An approval is requested in the autonomy gate |
approval.answered |
An approval is answered |
review.started |
A review run is started |
release.published |
A release is published |
connector.connected |
A connector credential is stored |
connector.revoked |
A connector credential is revoked |
webhook.test |
You run overlane webhook test; sent even if you did not subscribe to it |
A payload carries identifiers only, never a request body or a credential. Read the record through the API.
{ "id": "evt_0123456789abcdef", "tenantId": "your-team", "webhookId": "whk_0123456789abcdef", "sequence": 7, "type": "approval.requested", "createdAt": "2026-10-10T12:00:00.000Z", "apiVersion": "2026-10-10", "data": { "route": "autonomy.approvals.create", "method": "POST", "path": "/v1/tenants/your-team/autonomy/approvals" } }Headers
| Header | Meaning |
|---|---|
Overlane-Signature |
t=<unix milliseconds>, v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body> with the signing secret. While Overlane rotates its signing keys the header carries a second v1; accept the event when any v1 matches |
Overlane-Event-Id |
The event ID. This is your idempotency key |
Overlane-Sequence |
The webhook’s sequence number for this event, rising by one per event |
Verify an event
Check the raw bytes you received, compare in constant time, and reject a timestamp more than five minutes from your clock. The SDK does all three:
const problem = await verifyWebhook(secret, rawBody, signatureHeader);
assert.equal(problem, null);
const tampered = await verifyWebhook(secret, rawBody + " ", signatureHeader);
assert.equal(tampered, "mismatch");Without the SDK, check each v1 yourself (crypto.subtle.verify compares in constant time):
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
const pairs = signatureHeader.split(",").map((part) => part.trim().split("="));
const t = pairs.find(([name]) => name === "t")[1];
const signed = new TextEncoder().encode(`${t}.${rawBody}`);
const fromHex = (hex) => new Uint8Array((hex.match(/../g) ?? []).map((pair) => parseInt(pair, 16)));
let valid = false;
for (const [name, value] of pairs) if (name === "v1" && (await crypto.subtle.verify("HMAC", key, fromHex(value), signed))) valid = true;
assert.ok(valid);
assert.ok(Math.abs(Date.now() - Number(t)) < 300000);Retries and backoff
Answer with any 2xx to accept an event. Anything else, or no answer within four seconds, schedules another attempt:
| After failed attempt | Wait |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 10 minutes |
| 4 | 1 hour |
| 5 | 6 hours |
| 6 | 12 hours |
| 7 | 24 hours |
The eighth failure marks the event failed. A 4xx other than 408 and 429 is permanent and fails the event at once. overlane webhook deliveries shows the status, the attempts, the last HTTP status and when the next attempt is due.
assert.deepEqual([1, 2, 3, 4, 5, 6, 7].map(webhookBackoffSeconds), [30, 120, 600, 3600, 21600, 43200, 86400]);
assert.equal(webhookBackoffSeconds(8), null);
assert.equal(WEBHOOK_MAX_ATTEMPTS, 8);Order, replays and idempotency
Events for one webhook are sent in sequence order, and an event whose retry is not yet due holds back the ones behind it. Your endpoint may still see an event twice (you accepted it but your answer was lost) or out of order. Apply each event once, by its ID, and ignore one older than the last you applied; answer 2xx for a replay so Overlane stops retrying.
const state = { seenIds: new Set(), lastSequence: 0 };
const applied = [];
for (const event of [{ id: "a", sequence: 1 }, { id: "a", sequence: 1 }, { id: "c", sequence: 3 }, { id: "b", sequence: 2 }]) {
if (shouldApplyWebhookEvent(state, event)) {
state.seenIds.add(event.id);
state.lastSequence = event.sequence;
applied.push(event.id);
}
}
assert.deepEqual(applied, ["a", "c"]);Command reference: webhook create, webhook list, webhook test, webhook deliveries, webhook rotate and webhook delete.