If you are polling a news API on a timer, most of your requests return nothing new. Scoopkit published a median of 38 events a day across all 10 categories over the 45 days to 2026-09-04, so a job polling every 5 minutes burns 288 requests a day to catch them, and a job polling every hour catches them late. Webhooks invert that: you register a URL once, and Scoopkit posts each new event to it as it publishes, signed so you can verify it came from us. Subscriptions can filter by category or subcategory, so a service that only cares about model releases only gets woken for model releases. Webhooks are a Pro tier feature; everything else described here works the same on any tier.

Verified 2026-09-05 against the production delivery code in webhook_delivery.py and the live /v1/webhooks routes, not from documentation that might have drifted.

Creating a subscription

`

POST /v1/webhooks

{

"url": "https://example.com/hooks/scoopkit",

"filter": {"category": "MODEL_RELEASE"}

}

`

Two fields, and the schema rejects anything else. An empty filter matches every event. Setting category matches events where that category appears either as the primary category or anywhere in the event's categories array, so an event tagged with more than one category reaches you if any of them match. Setting subcategory is an exact match on the event's subcategory.

The response includes a secret. It is returned exactly once, at creation, and never again by GET /v1/webhooks. If you lose it, delete the subscription and make a new one.

Verifying the signature

Every delivery carries four headers:

HeaderContents
X-Scoopkit-Signaturesha256= followed by the HMAC hex digest
X-Scoopkit-TimestampUnix seconds, at send time
X-Scoopkit-Delivery-IdStable across retries of the same delivery
X-Scoopkit-Attempt1 through 5

The signed message is the timestamp, a literal period, then the raw request body:

`python

import hmac, hashlib

def verify(secret, timestamp, body, header):

message = f"{timestamp}.{body}".encode()

digest = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()

return hmac.compare_digest(f"sha256={digest}", header)

`

Two details that break implementations. Sign the raw body bytes as received, before any JSON parsing and re-serialization, because re-encoding changes whitespace and key order and the digest will not match. And compare with a constant-time function rather than ==.

The timestamp is there so you can reject deliveries that are too old to be legitimate. Nothing on our side enforces a maximum age, so that window is your choice.

Retries, and what a delivery failure costs you

A delivery that does not return a 2xx is retried up to 5 attempts total, with backoff of 1, 2, 4, 8, and 16 seconds between them. That is a full retry cycle inside about 31 seconds. After the fifth failed attempt the delivery is dead-lettered rather than retried forever.

This is a deliberately short and shallow schedule, and it is worth understanding before you rely on it. It handles a brief restart or a momentary blip. It does not handle your endpoint being down for an hour. If your receiver has real downtime, treat webhooks as the fast path and reconcile against GET /v1/events with a since parameter as the slow path. X-Scoopkit-Delivery-Id stays constant across retries of the same delivery, so deduplicating on it is straightforward.

Return 2xx as soon as you have durably accepted the payload, and do your real work afterward. A receiver that does 20 seconds of processing before responding will eat through the retry budget on its own.

The payload is just an event

The body is the same event envelope /v1/events returns, with the same fields and the same tier gating. There is no separate webhook schema to learn and no second set of field names to map. An event that arrives by webhook and the same event fetched by ID are byte-identical in their data object.

That includes deduplication. You get one delivery per event, not one per article, which is the entire point. The Stripe and OpenRouter acquisition in the funding data merged 9 articles into a single event, so a subscriber filtered to FUNDING_AND_DEALS got woken once for that story rather than nine times over several hours.

Runnable receivers in Python, Node, and curl, including a signature check tested against the production signing code, are in the public examples repo. Webhooks need a Pro key, which is on the pricing page.

FAQ

How many webhooks can I register?

Multiple subscriptions are supported, each with its own filter and its own secret. Delivery is evaluated per subscription, so an event matching two of your filters produces two deliveries.

Can I filter by company or keyword?

Not currently. Filtering is category and subcategory only. For anything narrower, subscribe at the category level and filter in your own receiver on the orgs array, which is a free-tier field.

What happens to events while my endpoint is down?

After 5 failed attempts a delivery is dead-lettered and not retried. Backfill with GET /v1/events?since=<timestamp>, which is the reason the archive window exists.

Do I get notified when an event is updated or merged?

The merge pass, which runs every cycle, can fold an event into another one after publication. Reconciling with since catches those; the webhook fires on publication.