Webhooks

Receive real-time notifications when briefings complete, fail, or subscriptions change.

Always verify the signature before processing the payload. Never trust unsigned requests.

Event types

Event Description
briefing.generated An AI briefing has been generated and its audio URL is ready.
briefing.failed A briefing job encountered an unrecoverable error.
subscription.upgraded The account was upgraded to a higher subscription tier.
subscription.cancelled The account's subscription was cancelled or expired.

Creating a webhook

Register an endpoint by sending a POST to /api/v1/webhooks. Specify the HTTPS URL to deliver events to and the list of event types you want to receive. Requires the webhooks:write scope.

curl -X POST https://listenbrief.com/api/v1/webhooks \
  -H "Authorization: Bearer lb_api_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhook",
    "events": ["briefing.generated", "briefing.failed"]
  }'

The response includes a secret field. Store this value immediately — it is used to verify incoming webhook signatures and is never shown again.

Payload format

Every webhook delivery is an HTTP POST to your URL with Content-Type: application/json. The body follows this shape:

{
  "event": "briefing.generated",
  "timestamp": 1717200000,
  "data": {
    "job_id": "job_abc123",
    "status": "generated"
  }
}

The timestamp field is a Unix epoch integer (seconds). The data object is event-specific; for briefing.generated it includes the job ID and final status.

Signature verification

Each delivery includes two headers that you must check before processing the payload:

Construct the signed message by concatenating the timestamp, a literal period, and the raw request body string. Compute an HMAC-SHA256 using your webhook secret, then compare using a constant-time equality function.

const crypto = require('crypto');

function verifyWebhook(secret, signature, timestamp, body) {
  const message = `${timestamp}.${body}`;
  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-listenbrief-signature'];
  const ts  = req.headers['x-listenbrief-timestamp'];
  if (!verifyWebhook(process.env.WEBHOOK_SECRET, sig, ts, req.body.toString())) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // process event...
  res.sendStatus(200);
});

Replay protection

The X-ListenBrief-Timestamp header lets you reject replayed requests. After verifying the signature, check that the timestamp is within 5 minutes of your server's current time. Reject any delivery whose timestamp falls outside this window.

const MAX_AGE_SECONDS = 5 * 60; // 5 minutes
const ts = parseInt(req.headers['x-listenbrief-timestamp'], 10);
if (Math.abs(Date.now() / 1000 - ts) > MAX_AGE_SECONDS) {
  return res.status(400).send('Timestamp too old');
}

Retry semantics

ListenBrief considers a delivery successful when your endpoint returns any 2xx HTTP status code within 10 seconds. If your endpoint returns a non-2xx status, closes the connection early, or times out, the delivery is retried with exponential backoff:

Maximum 5 attempts over approximately 30 minutes. Return 200 OK quickly — do any heavy processing asynchronously in a background queue.

Dead-letter handling

After all retry attempts are exhausted, the event is marked as failed and logged to your webhook delivery log. Visit your dashboard under Settings → Webhooks to inspect failed deliveries, view request/response details, and manually replay individual events if needed.