Skip to main content
x-cstar-request-id v2026-03

Spell Tome of Webhooks

— Don't Panic. The webhook will retry.

Register

const wh = await cstar.webhooks.create({
  name: 'Stripe ticket sync',
  url: 'https://api.your-app.com/cstar-webhooks',
  events: ['ticket.created', 'ticket.updated', 'ticket.closed']
});

// SAVE THIS NOW. Subsequent reads will not include it.
const secret = wh.signingSecret;

Verify (Node)

import express from 'express';
import { constructEvent } from '@cstar.help/js/webhook/node';

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    try {
      const event = constructEvent(
        req.body.toString('utf8'),
        req.headers['x-signature'],
        process.env.CSTAR_WEBHOOK_SECRET
      );
      res.status(200).json({ received: true });
    } catch (err) {
      res.status(401).json({ error: err.message });
    }
  }
);

Verify (Edge / Workers)

import { constructEventAsync } from '@cstar.help/js/webhook';

export async function POST(request) {
  const body = await request.text();
  try {
    const event = await constructEventAsync(
      body,
      request.headers.get('x-signature'),
      process.env.CSTAR_WEBHOOK_SECRET
    );
    return new Response('ok', { status: 200 });
  } catch (err) {
    return new Response(err.message, { status: 401 });
  }
}

Headers

  • X-Signaturet=<unix>,v1=<hex> Stripe-style.
  • X-Event-Type — e.g. ticket.created.
  • X-Event-ID — unique. Use as your idempotency key.
  • X-Webhook-ID — your subscription's ID.
  • X-Timestamp — ISO 8601 send time.
  • X-Delivery-Attempt — starts at 1.
  • x-cstar-request-id — the API call that triggered this.

Local dev

# Forward live events to localhost
cstar listen --forward-to http://localhost:3000/webhook

# Fire a synthetic event into every active receiver
cstar trigger ticket.created
cstar trigger boss.spawned --json

Gotchas

  • signingSecret is only on the create response. Lose it and you rotate.
  • Use the raw body for verification. JSON.parse first and the HMAC fails.
  • Replay window defaults to 300s. Pass { tolerance: <seconds> } to override.
  • Legacy sha256=<hex> still verified; no longer emitted.
  • 10s response budget. Anything slower and the delivery times out — auto-retries follow.