Skip to content

Webhooks

Outbound webhooks are signed with HMAC-SHA256, carry an event ID and a sequence, retry with exponential backoff, and are safe to replay. How to register one and how to verify an event.

Updated View as MarkdownOpen in ChatGPTOpen in Claude

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_0123456789abcdef

The 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close

Overlane Agent

Ask about the docs. Answers are quoted from the pages, with links. This panel cannot change anything.

    Questions are answered in your browser from the docs index. Nothing you type here is stored or sent anywhere.