Webhooks
Archon can notify another system the moment something happens — a sample completes a stage, a notebook entry is finalized — instead of that system having to poll the API.
Setting one up
An owner or admin creates a webhook at Settings → Integrations → Webhooks: a destination URL, an optional signing secret, and which events it should fire on. A Send test button delivers a ping event immediately, so you can confirm the receiving end before anything real depends on it.
Events
| Event | Fires when |
|---|---|
sample.created | A new sample is logged. |
sample.updated | A sample’s data or stage changes. |
sample.completed | A sample finishes its workflow. |
notebook.created | A new notebook entry or folder is created. |
notebook.finalized | An entry is finalized and signed. |
protocol.executed | A protocol run against a sample completes. |
sop.completed | An SOP acknowledgement or execution completes. |
inventory.low_stock | A stock item crosses its reorder threshold. |
Payload
Every delivery is a JSON POST with the event name and timestamp folded into the body:
Content-Type: application/json
User-Agent: Archon-Webhook/1.0
X-Archon-Event: notebook.finalized
X-Archon-Signature: sha256=<hmac hex digest>{
"event": "notebook.finalized",
"timestamp": "2026-09-01T14:32:07.000Z",
"notebookId": "b3d1...",
"title": "Run 14 — extraction QC"
}Verifying the signature
If you set a signing secret when creating the webhook, every delivery is signed with HMAC-SHA256 over the raw request body. Verify it before trusting the payload:
import { createHmac, timingSafeEqual } from "crypto";
function isValidSignature(rawBody: string, header: string | null, secret: string): boolean {
if (!header?.startsWith("sha256=")) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const given = header.slice(7);
return (
expected.length === given.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(given))
);
}Compare against the raw request body, before any JSON parsing — re-serializing the parsed object is not guaranteed to produce byte-identical output, which would make a valid signature look wrong.
Delivery behaviour
- 10-second timeout. A slow endpoint counts as a failed delivery rather than holding the request open.
- No redirects followed. A
3xxresponse is treated as a failure — an attacker-controlled redirect could otherwise be used to reach an internal address. - Auto-pause after 5 consecutive failures. The webhook is marked
errorin the dashboard rather than retrying forever against a dead endpoint; re-enable it once the destination is fixed. - Delivery history is kept — status code and response body (capped at 2 KB) for each attempt, visible from the webhook’s row in Settings.
- Deliveries for the same event fire in parallel to all active webhooks subscribed to it, not sequentially.
Outbound targets are re-validated on every single delivery, not just when the webhook is created — the destination’s hostname is resolved and checked against loopback, link-local, private and cloud-metadata address ranges each time, which defends against a hostname being re-pointed at an internal address after the webhook was approved (DNS rebinding). If your deployment restricts webhook destinations to an allowlist (WEBHOOK_ALLOWED_HOSTS), your endpoint’s host needs to be on it.