Webhooks
Webhooks push wipe and certificate events from AVERASE to your systems the moment they happen. Each webhook subscribes one endpoint to one event type; every delivery is signed with the webhook's secret so your receiver can prove it came from AVERASE.
What webhooks push
There are six event types, covering the full life of a session — from the moment a wipe is authorized to the moment its certificate PDF is ready:
| Event type | Fires when |
|---|---|
wipe_session_ready | A pre-wipe check-in is allowed and a wipe is about to start |
wipe_session_blocked | A boot verification or check-in is denied |
asset_reported | A machine boots from AVERASE for the first time |
hardware_tests_completed | An operator submits hardware test results |
wipe_session_completed | A wipe result arrives — success or failure |
certificate_generated | A certificate of erasure is signed and its PDF uploaded |
A webhook subscribes to exactly one event type. To receive several event types, create one webhook per type — they can all point at the same URL, and the X-Erase-Engine-Event header tells your receiver which one fired.
Creating a webhook
Webhooks are managed by account admins under Settings → Webhooks in the web application. The index lists every webhook with its Event type, URL, Status (Active, Disabled, or Auto-disabled — "off after N straight failures"), Last triggered time, and an Edit link, plus a New Webhook button in the header band. As the page itself puts it:
Push wipe and certificate events to your systems the moment they happen. Each webhook subscribes one endpoint to one event type; payloads are signed with the webhook's secret (X-Erase-Engine-Signature).
Form fields
- Name
Event type- Type
- select
- Description
One of the six event types. One event type per webhook.
- Name
URL- Type
- string
- Description
Your receiving endpoint. Must be
https://— plain HTTP is rejected. Placeholder:https://example.com/webhooks/erase-engine.
- Name
Auth type- Type
- select
- Description
None (signature only), Bearer token, or Basic auth. The form explains it as: "How we authenticate to YOUR endpoint (separate from our payload signature)." When Bearer or Basic is selected, deliveries carry an
Authorizationheader in addition to the signature. Credentials are encrypted at rest; leaving a secret field blank when editing keeps the stored value.
- Name
Active- Type
- checkbox
- Description
Shown when editing. "Inactive webhooks receive nothing; re-enabling redelivers held events."
The signing secret
Every webhook gets an auto-generated signing secret (64 hex characters) the moment it is created — you never choose it. The edit page shows the secret in full, along with the verification recipe:
Verify each delivery by computing HMAC-SHA256 over
"{X-Erase-Engine-Timestamp}.{raw body}"with this secret and comparing it to X-Erase-Engine-Signature (format: sha256=hex).
Send test event
The edit page's Send test event button POSTs a synthetic ping to your endpoint synchronously — same three signing headers as a real delivery, so you can exercise your verification code end to end. The test body carries no record fields:
Test ping body
{
"event_id": "f6a7b8c9-d0e1-4234-9f4a-5b6c7d8e9f0a",
"event_type": "wipe_session_completed",
"test": true,
"created_at": "2026-08-12T17:00:05Z"
}
Test pings are not recorded in the delivery log. The result comes back immediately as a flash: "Test event delivered — your endpoint answered 200." on success, or "Test event failed: <error>" on failure.
Deleting a webhook
Delete asks for confirmation: "Delete this webhook? Undelivered events for it will stop retrying." Deleting a webhook abandons any deliveries still in its retry queue.
Delivery log
Below the webhook list, a delivery log shows recent deliveries: Event (event type plus the event_id in monospace), Endpoint, Status (delivered / failed / pending), and When.
The envelope
Every delivery is a single JSON object: three envelope fields, then the record's fields flat at the top level — there is no nested payload or data wrapper, and no batching (one record per POST).
Envelope shape (truncated)
{
"event_id": "d4e5f6a7-b8c9-4012-9d2e-3f4a5b6c7d8e",
"event_type": "wipe_session_completed",
"created_at": "2026-08-12T17:41:08Z",
"id": "wr_01J4R2M8K3P7QS5TV9WX6YZABC",
"type": "wipe_report",
"updated_at": "2026-08-12T17:41:08Z",
"revision": 1786815668,
"serial": "S64FNE2R504071",
"status_display": "Completed"
}
- Name
event_id- Type
- uuid
- Description
Unique per delivery. Retries of the same delivery reuse it — dedupe on this field.
- Name
event_type- Type
- string
- Description
One of the six event types; also sent as the
X-Erase-Engine-Eventheader.
- Name
created_at- Type
- timestamp
- Description
When the event was created. Record fields are spread after the envelope fields, so if the record carries its own
created_at, the record's value wins.
Every record additionally carries id (its stable identifier — asset_…, wr_…, cert_…, or a job UUID), type (the record type), updated_at, and revision (the Unix-seconds form of updated_at) — the keys your upsert logic relies on. See Payloads for the full record contract.
Verifying deliveries
Every delivery carries three signing headers:
| Header | Value |
|---|---|
X-Erase-Engine-Timestamp | Unix seconds at send time |
X-Erase-Engine-Signature | sha256= + lowercase hex HMAC-SHA256 over "{timestamp}.{raw body}", keyed with the webhook's signing secret |
X-Erase-Engine-Event | The event type, e.g. wipe_session_completed |
Content-Type is always application/json, and an Authorization: Bearer … or Basic … header is added when the webhook's auth type calls for it.
The signature covers the exact JSON bytes of the body — retries resend the same frozen bytes, so the signature stays stable across attempts. Verify against the raw request body before parsing it; a re-serialized body will not match. Use a constant-time comparison, and reject timestamps more than 300 seconds from your clock to shut down replay.
Verifying a delivery
const crypto = require('node:crypto')
// rawBody must be the exact request bytes — verify BEFORE JSON.parse.
function verify(req, rawBody, signingSecret) {
const timestamp = req.headers['x-erase-engine-timestamp']
const signature = req.headers['x-erase-engine-signature']
const expected =
'sha256=' +
crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300
const valid =
signature &&
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
return fresh && valid
}
A delivery counts as successful when your endpoint answers with any 2xx status within 10 seconds (connection open 5 s, read/write 10 s). Anything else — a non-2xx status, a timeout, a connection failure — counts as a failed attempt.
Delivery and retries
Deliveries are durable. A ledger row is written for every (webhook, event) pair before the first attempt, so a crash mid-send can never lose an event — it can only leave it pending, where reconciliation picks it up.
- 8 attempts per delivery, spaced with polynomial backoff (plus jitter), spanning roughly 90 minutes end to end.
- Per-endpoint serialization: each webhook delivers one event at a time, so a slow endpoint queues rather than getting hammered concurrently. Ordering across event types is still not guaranteed — treat every event as an upsert.
- A successful delivery resets the webhook's consecutive-failure count to zero.
- A delivery that exhausts all 8 attempts is marked failed and counts one consecutive failure against the webhook.
Auto-disable and recovery
After 5 consecutive exhausted deliveries — five whole deliveries, each having burned all 8 attempts — the webhook is disabled automatically and its status shows Auto-disabled in Settings → Webhooks. The edit page explains:
This webhook was disabled automatically on
<date>after<count>consecutive delivery failures. Fix the endpoint, then re-enable it below — held events will be redelivered.
Fix your endpoint, then tick Active and save. Re-enabling resets the failure count and triggers reconciliation, which redelivers the events your endpoint missed.
Reconciliation and backfill
A reconciliation job sweeps up anything the retry pipeline could not finish:
- Re-queues deliveries marked failed, and any left pending for more than an hour (e.g. after a crash between ledger write and first attempt).
- Backfills records that never got a delivery row at all — in batches of 500 — for the four record-shaped events:
asset_reported,hardware_tests_completed,wipe_session_completed, andcertificate_generated(certificates are only backfilled once theirpdf_urlis set).
Backfilled and re-queued deliveries get new event_ids.
wipe_session_ready and wipe_session_blocked are never backfilled —
they describe moments, not records. If your endpoint is down when a session
starts or is blocked, that event is gone; everything durable about the
session still arrives later via wipe_session_completed and
certificate_generated.
Subscriber best practices
- Dedupe on
event_id. Retries reuse it; reconciliation mints new ones — so the same underlying record can arrive under more than oneevent_id. Which is why you also: - Upsert on
id+revision. Apply a record when itsrevisionis greater than or equal to the one you have stored; ignore stale ones. - Don't rely on ordering. Deliveries to one endpoint are serialized, but ordering across event types is not guaranteed —
certificate_generatedcan plausibly land before thewipe_session_completedit belongs to. - Expect bursts after outages. When your endpoint recovers or a webhook is re-enabled, reconciliation redelivers everything held — potentially hundreds of events in quick succession.
- Enforce the ±300 s timestamp window and a constant-time signature comparison, as in the snippets above.
- Respond 2xx fast. You have 10 seconds; queue the payload and process it asynchronously rather than doing work inline.
Testing
The sandbox runs the same event types, envelope, signing headers, and record schemas locally — including a live webhook viewer that shows every delivery, its headers, and whether its signature verifies. It is the fastest way to build a receiver before pointing production at it.