Webhook testing
The sandbox delivers real, signed webhooks for every event the platform fires — so you can build and debug a receiver locally, or skip the receiver entirely and let the built-in sink capture deliveries for inspection.
The event catalog, envelope, and record schemas are the production ones — see Webhooks, Event types, and Payloads. This page covers the sandbox-side machinery: creating subscriptions, firing deliveries, and reading what happened. All examples use https://api.averase.com.
Create a subscription
Each subscription targets exactly one event type — to receive several event types, create several subscriptions. The six event types:
| Event type | Fired from |
|---|---|
wipe_session_ready | validate allowed |
wipe_session_blocked | verify or validate denied |
asset_reported | POST /internal/assets, first boot of a machine |
hardware_tests_completed | POST /internal/assets/tests |
wipe_session_completed | POST /internal/wipe_results, first receipt |
certificate_generated | wipe result that is certifiable |
Required attributes
- Name
event_type- Type
- string
- Description
One of the six event types above.
Optional attributes
- Name
account_id- Type
- string
- Description
Account the subscription belongs to. Defaults from your API key when one is attached; without either you get
422 {"error":"account_id required (or authorize with an API key)"}.
- Name
url- Type
- string
- Description
Where deliveries go. Defaults to
sandbox://sink, the built-in capture target. Realhttp(s)URLs are POSTed for real.
- Name
auth_type- Type
- string
- Description
none,bearer, orbasic, plus the matching credentials — adds anAuthorizationheader to every delivery.
The 201 response includes the subscription's signing_secret (64 hex
characters) — the HMAC key for verifying deliveries. Save it; it's what
your receiver checks signatures against.
Request
curl https://api.averase.com/sandbox/webhooks \
-H "Content-Type: application/json" \
-d '{
"account_id": "acct_demo",
"event_type": "wipe_session_completed",
"url": "sandbox://sink"
}'
Response (201)
{
"prefix_id": "whk_01J4R0A2B3C4D5E6F7G8H9JKLM",
"account_id": "acct_demo",
"event_type": "wipe_session_completed",
"url": "sandbox://sink",
"signing_secret": "9c1f2e3d4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d",
"auth_type": "none",
"active": true,
"failure_count": 0,
"auto_disabled_at": null
}
Manage subscriptions
GET /sandbox/webhooks lists every subscription. PATCH updates
active, url, or event_type — and re-enabling a subscription
resets its failure budget (failure_count back to zero,
auto_disabled_at cleared), so a receiver you've fixed gets a clean
slate. DELETE removes the subscription and its delivery rows and
returns {"ok": true}.
Request
curl -X PATCH https://api.averase.com/sandbox/webhooks/whk_01J4R0A2B3C4D5E6F7G8H9JKLM \
-H "Content-Type: application/json" \
-d '{"active": true}'
# failure_count → 0, auto_disabled_at → null
Send a test ping
Fires a synthetic, signed ping at the subscription's URL — the
fastest way to confirm your receiver's signature verification before any
real events flow. The body is
{event_id, event_type, "test": true, created_at}: no record fields, and
no row is written to the delivery ledger.
You get {"ok": true, "response_code": 200} when the receiver accepted
it, or {"ok": false, "error": "HTTP 500"} when it didn't.
Request
curl -X POST https://api.averase.com/sandbox/webhooks/whk_01J4R0A2B3C4D5E6F7G8H9JKLM/test
Response (200)
{
"ok": true,
"response_code": 200
}
The delivery ledger
Every real delivery attempt, newest 100 first. Each row records the
account_id, event_id, event_type, the payload (parsed — these are
the exact bytes that were signed), status (pending, delivered, or
failed), attempts, response_code, last_error, delivered_at,
created_at, and the subscription's webhook_prefix_id and
webhook_url.
Deliveries are deduplicated per subscription per event: one row per
(webhook, event_id).
Request
curl https://api.averase.com/sandbox/webhook_deliveries
Response (200)
[
{
"account_id": "acct_demo",
"event_id": "d4e5f6a7-b8c9-4012-9d2e-3f4a5b6c7d8e",
"event_type": "wipe_session_completed",
"payload": { "id": "wr_01J4R2M8K3P7QS5TV9WX6YZABC" },
"status": "delivered",
"attempts": 1,
"response_code": 200,
"last_error": null,
"delivered_at": "2026-08-12T17:41:08Z",
"created_at": "2026-08-12T17:41:08Z",
"webhook_prefix_id": "whk_01J4R0A2B3C4D5E6F7G8H9JKLM",
"webhook_url": "sandbox://sink"
}
]
The built-in sink
A subscription pointed at sandbox://sink (the default) never touches
the network: the delivery bypasses HTTP entirely, always succeeds, and
writes a capture row with the headers, the body, and a recomputed
signature_valid — the sandbox re-verifies its own signature the way
your receiver would, so a true here is a working reference check.
GET /sandbox/sink returns the newest 100 captures:
webhook_prefix_id, event_type, signature_valid, headers, body,
received_at.
Point the subscription at a real http(s) URL instead and the same
delivery is POSTed for real — that's how you exercise your own receiver.
Request
curl https://api.averase.com/sandbox/sink
Response (200)
[
{
"webhook_prefix_id": "whk_01J4R0A2B3C4D5E6F7G8H9JKLM",
"event_type": "wipe_session_completed",
"signature_valid": true,
"headers": {
"Content-Type": "application/json",
"X-Erase-Engine-Timestamp": "1786815668",
"X-Erase-Engine-Signature": "sha256=…",
"X-Erase-Engine-Event": "wipe_session_completed"
},
"body": "{\"event_id\":\"d4e5f6a7-…\",…}",
"received_at": "2026-08-12T17:41:08Z"
}
]
The live viewer
GET https://api.averase.com/webhooks serves an HTML page ("Webhooks · AVERASE Sandbox") that shows the whole webhook surface at once and auto-refreshes every 3 seconds — leave it open in a tab while you drive the flow from curl. Its sections:
- API key — a box for your web application API key, with a status pill:
no key — anonymous sandbox mode,key accepted — new tokens/webhooks bind to your account, orinvalid key (webapp rejected it). The key is shared with the/liveconsole via browser localStorage. - Subscriptions — create subscriptions, Test / Enable–Disable / Delete each one, reveal its
signing_secret, and read its state pill (active/auto-disabled/disabled). - Delivery ledger — the same rows as
GET /sandbox/webhook_deliveries. - Sink captures — the same rows as
GET /sandbox/sink, including each capture'ssignature_valid.
The failure budget
The sandbox keeps production's auto-disable rule but not its retry queue:
- A delivery row is inserted (
pending) before the attempt, then the sandbox makes one immediate attempt with a 10-second timeout. Success — any 2xx — marks itdeliveredand resetsfailure_countto zero; failure marks itfailedand incrementsfailure_count. - At 5 consecutive failures the subscription is auto-disabled (
activefalse,auto_disabled_atset). Re-enabling it via PATCH or the live viewer resets the budget. - There is no retry — a failed delivery in the sandbox stays failed, so the budget counts single attempts.
Production behaves differently: each delivery gets up to 8 attempts with polynomial backoff and jitter over roughly 90 minutes, the ledger is durable, and re-enabling a subscription triggers reconciliation. Don't infer production delivery timing from the sandbox — see Webhooks for the real contract.
Verifying signatures
Sandbox deliveries are signed exactly like production ones:
Content-Type: application/json
X-Erase-Engine-Timestamp: 1786815668
X-Erase-Engine-Signature: sha256=<lowercase hex HMAC-SHA256(signing_secret, "{timestamp}.{body}")>
X-Erase-Engine-Event: wipe_session_completed
Your receiver should recompute the HMAC over the timestamp, a dot, and the raw request body, compare in constant time, and enforce a ±300-second skew window:
const crypto = require("crypto")
const expected =
"sha256=" +
crypto
.createHmac("sha256", SIGNING_SECRET)
.update(`${req.headers["x-erase-engine-timestamp"]}.${rawBody}`)
.digest("hex")
// compare against req.headers["x-erase-engine-signature"] in constant time
// (crypto.timingSafeEqual), and reject timestamps more than 300s off
Respond with any 2xx within 10 seconds to count as delivered. If the subscription sets auth_type, the delivery also carries the matching Authorization: Bearer … or Basic … header.
Worked example: capture a wipe report
Subscribe to wipe_session_completed, run a wipe through the mirror, then read the capture out of the sink.
1. Subscribe (default URL is the sink):
curl https://api.averase.com/sandbox/webhooks \
-H "Content-Type: application/json" \
-d '{"account_id": "acct_demo", "event_type": "wipe_session_completed", "url": "sandbox://sink"}'
2. Run the flow — mint a token, validate, post a wipe result (the full sequence is the end-to-end walkthrough):
JTI=$(curl -s https://api.averase.com/sandbox/tokens \
-H "Content-Type: application/json" \
-d '{"account_name": "Test Co", "account_id": "acct_demo", "user_email": "ops@testco.example"}' \
| jq -r .jti)
JOB=$(curl -s https://api.averase.com/internal/tokens/validate \
-H "Content-Type: application/json" \
-d "{\"jti\": \"$JTI\", \"account_id\": \"acct_demo\"}" | jq -r .job_id)
curl https://api.averase.com/internal/wipe_results \
-H "Content-Type: application/json" \
-d "{
\"jti\": \"$JTI\",
\"job_id\": \"$JOB\",
\"drive_serial\": \"S64FNE2R504071\",
\"drive_model\": \"SAMSUNG MZVL2512HDJD-00BL2\",
\"drive_capacity_gb\": 512,
\"wipe_method\": \"NIST 800-88 R2 Purge\",
\"technique\": \"NVMe Sanitize — crypto erase\",
\"wipe_status\": \"completed\",
\"validation\": \"approved\",
\"started_at\": \"2026-08-12T17:03:01Z\",
\"completed_at\": \"2026-08-12T17:40:58Z\"
}"
3. Inspect the capture — curl https://api.averase.com/sandbox/sink (or watch it appear in the live viewer). The capture's body is the full signed wipe_session_completed payload:
{
"event_id": "d4e5f6a7-b8c9-4012-9d2e-3f4a5b6c7d8e",
"event_type": "wipe_session_completed",
"id": "wr_01J4R2M8K3P7QS5TV9WX6YZABC",
"type": "wipe_report",
"product": "engine",
"created_at": "2026-08-12T17:41:08Z",
"updated_at": "2026-08-12T17:41:08Z",
"revision": 1786815668,
"job_id": "0e6f4a8c-2d1b-4e5f-9a7c-8b6d5e4f3a2b",
"serial": "S64FNE2R504071",
"model": "SAMSUNG MZVL2512HDJD-00BL2",
"capacity_bytes": 512000000000,
"capacity_display": "512 GB",
"erase_algorithm": "nist_800_88_r2_purge",
"erase_algorithm_display": "NIST 800-88 R2 Purge",
"erase_technique": "NVMe Sanitize — crypto erase",
"status_display": "Completed",
"erase_result_display": "Pass",
"erase_start_time": "2026-08-12T17:03:01Z",
"erase_finish_time": "2026-08-12T17:40:58Z",
"certificate_status": "pending",
"certificate_url": null,
"license_token_id": "ltok_01J4QY7R5N8T2VW9XA3BC4DEFG",
"operator": "ops@testco.example"
// …full field list on the Payloads page
}
Because the wipe was certifiable, a certificate_generated event fired too — but only subscriptions for that event type receive it, which is exactly why one subscription covers one event type. Dedupe on event_id: it is unique per delivery, and a redelivery of the same event reuses it. The full record schemas — including every field of the wipe report above — live on Payloads.