Skip to content
API / Webhook events and signatures
  1. Docs
  2. API
  3. Webhook events and signatures

Webhook events and signatures

Signed webhooks when a scan completes or fails or an automatic top-up is declined: the payload, HMAC-SHA256 checks in Node.js and Python, and retries.

Open .md

Instead of polling, let Space Context call you. Add an endpoint in the console under Webhooks: an HTTPS URL, the events, and the mode (test events come from test keys, live events from live keys). The signing secret (whsec_…) is shown once.

Events

EventWhendata
scan.completedA scan finished{ "scan": Scan }
scan.failedA scan failed and was refunded{ "scan": Scan } with error and charge.refunded: true
wallet.auto_recharge_failedThe bank declined an automatic top-up; it was turned off{ "reason", "amount_cents", "auto_recharge_enabled": false }

Request

HTTP
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SpaceContext-Webhooks/1
SpaceContext-Event-Id: evt_scn_8a…_completed
SpaceContext-Signature: t=1791200000,v1=5f2c…

{
  "id": "evt_scn_8a…_completed",
  "object": "event",
  "type": "scan.completed",
  "livemode": true,
  "created_at": "2026-10-05T10:15:03.000Z",
  "data": { "scan": { "object": "scan", "id": "scn_8a…", "space_id": "spc_3f…", "status": "complete", … } }
}

Answer with any 2xx within 10 seconds. Do the work after answering.

Verify the signature

SpaceContext-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256> of "<t>.<raw body>", keyed with the whole signing secret (including whsec_). Verify it on the raw body, compare in constant time, and reject old timestamps:

TypeScript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifySpaceContext(rawBody: string, header: string, secret: string, toleranceSeconds = 300): boolean {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=') as [string, string]));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}
Python
import hashlib, hmac, time

def verify_spacecontext(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Delivery

  • At least once. An event can arrive more than once: use its id (also in SpaceContext-Event-Id) to ignore repeats. Ids are stable for the same fact: a scan completes once, so evt_<scan>_completed is sent once per endpoint even if the pipeline repeats a step.
  • Retries. A failed delivery (no 2xx, timeout, network error) is retried up to 8 times, waiting 30 seconds, then doubling up to an hour between attempts.
  • Auto-disable. After 20 failed deliveries in a row the endpoint is turned off; turn it back on in the console.
  • Order is not guaranteed.
  • Never redirected. A 3xx is a failure.

Allowed URLs

https on port 443, with a public hostname: no IP addresses, no localhost or private names, no Space Context or Cloudflare hosts. The URL is checked when you add the endpoint and before every delivery. For local development, use a tunnel with a public HTTPS name.

In the console

Send test event delivers a sample event (data: { "test": true }) and shows the answer. Deliveries lists recent attempts with their status and the last answer. Up to 10 endpoints per mode (10 for test events, 10 for live).

Webhooks in the console: add an HTTPS endpoint for test or live events, choose scan.completed, scan.failed and wallet.auto_recharge_failed, and see your endpoints

Space Context docs · llms.txt · download Edit on GitHub

esc

↑↓ navigate↵ openesc close