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.

POST/sandbox/webhooks

Create a subscription

Each subscription targets exactly one event type — to receive several event types, create several subscriptions. The six event types:

Event typeFired from
wipe_session_readyvalidate allowed
wipe_session_blockedverify or validate denied
asset_reportedPOST /internal/assets, first boot of a machine
hardware_tests_completedPOST /internal/assets/tests
wipe_session_completedPOST /internal/wipe_results, first receipt
certificate_generatedwipe 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. Real http(s) URLs are POSTed for real.

  • Name
    auth_type
    Type
    string
    Description

    none, bearer, or basic, plus the matching credentials — adds an Authorization header 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

POST
/sandbox/webhooks
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
}

GET/PATCH/DELETE/sandbox/webhooks/:id

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

PATCH
/sandbox/webhooks/:id
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

POST/sandbox/webhooks/:id/test

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

POST
/sandbox/webhooks/:id/test
curl -X POST https://api.averase.com/sandbox/webhooks/whk_01J4R0A2B3C4D5E6F7G8H9JKLM/test

Response (200)

{
  "ok": true,
  "response_code": 200
}

GET/sandbox/webhook_deliveries

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

GET
/sandbox/webhook_deliveries
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"
  }
]

GET/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

GET
/sandbox/sink
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, or invalid key (webapp rejected it). The key is shared with the /live console 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's signature_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 it delivered and resets failure_count to zero; failure marks it failed and increments failure_count.
  • At 5 consecutive failures the subscription is auto-disabled (active false, auto_disabled_at set). 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.

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

Was this page helpful?