Skip to content

Webhooks overview

Webhooks push email events to your server as they happen, so you do not have to poll GET /emails/{id}. SpacePost signs every request with the Standard Webhooks scheme (the one Svix and Resend use), so existing verification code works unchanged.

  1. Create an endpoint that accepts POST with a JSON body and answers 2xx quickly.

  2. Register it

    create-webhook.ts
    import { SpacePost } from "@spacepost/node";
    const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
    const { data: webhook, error } = await spacepost.webhooks.create({
    endpoint: "https://acme.com/webhooks/spacepost",
    events: ["email.delivered", "email.bounced", "email.complained"],
    });
    if (error) throw new Error(`${error.name}: ${error.message}`);
    // Store this secret: you need it to verify signatures. It starts with "whsec_".
    console.log(webhook.id, webhook.signing_secret);
    Response
    {
    "object": "webhook",
    "id": "0192e1c4-0000-7000-8000-0000000000aa",
    "endpoint": "https://acme.com/webhooks/spacepost",
    "events": ["email.delivered", "email.bounced", "email.complained"],
    "status": "enabled",
    "created_at": "2026-10-23T14:00:00.000Z",
    "signing_secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
    }
  3. Verify the signature on every request. See Verifying signatures.

  4. Return 2xx. Anything else (including redirects and timeouts) is retried. See Retries.

Every delivery is a POST with Content-Type: application/json and these headers:

Header Value
webhook-id Unique id of this delivery. The same id on every retry, so use it to deduplicate.
webhook-timestamp Unix time in seconds when this attempt was sent.
webhook-signature v1,<base64 HMAC-SHA256>. A space-separated list if there are several signatures.
svix-id, svix-timestamp, svix-signature Identical duplicates of the three headers above, so Svix libraries work as is.

The body is { "type": "...", "created_at": "...", "data": { ... } }. See Event types.

POST /webhooks, GET /webhooks, GET /webhooks/{id}, PATCH /webhooks/{id} (change endpoint, events or status: enabled or disabled) and DELETE /webhooks/{id}. The signing_secret is returned when you create or fetch a single webhook, not in lists.

manage-webhooks.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const id = "0192e1c4-0000-7000-8000-0000000000aa";
// Subscribe to more events and temporarily disable delivery.
await spacepost.webhooks.update({ id, events: ["email.delivered", "email.bounced", "email.opened"], status: "disabled" });
const { data } = await spacepost.webhooks.list({ limit: 20 });
console.log(data?.data.map((w) => `${w.id} ${w.status} ${w.endpoint}`));
await spacepost.webhooks.remove(id);
  • https:// in production (plain http:// is accepted only in development).
  • The host must resolve to a public address. Localhost and private networks are rejected. For local development use a tunnel such as ngrok or Cloudflare Tunnel.
  • Respond within 15 seconds.
  • Redirects are not followed.