Webhooks
Rather than polling for a story that breaks or a gap that opens, have us post it to you. Webhooks are an Enterprise scope.
Setting one up
In the console, give a public HTTPS URL, the topics you care about (leave it empty for all of them), and whether you want breaking, blindspot, or both. You get a signing secret once, at creation.
The destination must be a public HTTPS hostname on port 443. Private addresses, link-local ranges, credentials in the URL, fragments and non-standard ports are rejected when you save, and the hostname is resolved again before every delivery, so a domain that later points inside a private network stops being delivered to.
Five enabled endpoints are included. Disable one before adding a sixth.
The delivery
POST /your/receiver HTTP/1.1
Content-Type: application/json
User-Agent: baba-News-Webhooks/1.0
X-Baba-Event-Id: 3a1e6c88-5b1d-4a2f-9d43-0b7c6e0a11f2
X-Baba-Signature: t=1788350400,v1=4c1b...9ea3
{
"id": "3a1e6c88-5b1d-4a2f-9d43-0b7c6e0a11f2",
"type": "story.blindspot",
"storyId": "44",
"state": "blindspot",
"topic": "politics",
"headline": "Cabinet approves the deal",
"url": "https://news.itsbaba.com/812/cabinet-approves-the-deal",
"observedAt": "2026-09-01T08:14:22.000Z"
}type is story.breaking or story.blindspot. storyId is the cluster id, so the story detail endpoint gives you the full record.
Verifying the signature
X-Baba-Signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<t>.<raw body>"). Verify against the raw request bytes, before any JSON parsing, and compare in constant time. Reject a timestamp more than five minutes from now.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody is the exact request bytes, before JSON parsing.
function verifyBabaWebhook(rawBody, header, secret) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return false;
const expected = createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}
// After verification: persist event.id with a unique constraint, enqueue work,
// and return 2xx. A duplicate id must return 2xx without repeating the action.Retries
Answer 2xx within ten seconds. Anything else, including a timeout, is retried on a fixed ladder.
| Attempt | When |
|---|---|
| 1 | Immediately |
| 2 | 1 minute later |
| 3 | 5 minutes later |
| 4 | 15 minutes later |
| 5 | 1 hour later |
| 6 | 4 hours later |
A 4xx other than 408 or 429 stops the ladder at once: it says the request itself is wrong, and repeating it will not help. After six attempts the delivery is marked failed and is not retried again.
Delivery is at-least-once. Make your handler idempotent on id.