Countdown webhooks
Webhooks replace polling. Instead of asking whether a countdown has ended, you register an endpoint once and we call it — at zero, or at any point you nominate along the way. This page is the map: the three objects involved, how they relate, and the three requests that get a working webhook in place. The pages that follow go into each piece.
Why not poll
Polling works and is simple, and it stops working at exactly the point your product starts mattering. The cost scales with the number of timers multiplied by how fresh you need the answer, and the freshness is bounded by your interval no matter how much you spend.
The same job, two ways
// Polling: one request per timer per interval, and you find out
// somewhere between 0 and 60 seconds late.
setInterval(async () => {
for (const timerId of activeTimers) { // 500 timers
const { data } = await call(`/timers/${timerId}/status`);
if (data.ended) await releaseReservation(timerId);
}
}, 60_000); // = 720,000 requests/day
// Webhooks: one request when it actually happens.
app.post("/webhooks/countdownshare", async (req, res) => {
res.sendStatus(200); // acknowledge first
await queue.add("release-reservation", req.body);
});| Polling | Webhooks | |
|---|---|---|
| Requests for 500 timers | 720,000 per day at 1-minute intervals | One per event |
| How late you find out | Up to your interval | Seconds |
| Rate limit pressure | Grows with every timer added | Independent of timer count |
| Works while your service is down | No — the poll simply does not happen | Yes — deliveries retry for up to about 15 hours |
Three objects
The separation exists so one endpoint can serve many timers without you re-entering a URL and a secret every time.
How they relate
Destination Rule Delivery
(account-level) (per timer) (per firing)
"my order service" <---- "1 hour left" ----> attempt 1: 500
https://…/hooks on timer A attempt 2: 200 OK
whsec_… status: succeeded
^
| <---- "ended" ----> attempt 1: 200 OK
| on timer A status: succeeded
|
+------------- <---- "ended" ----> …
on timer B
One destination. Many rules across many timers.Destination
A URL and a signing secret, registered once at the account level and reused by every rule that points at it. Your plan limits how many you can have.
Rule
A single condition on a single timer, pointing at one destination, delivered once or on repeat. Rules are unlimited on every plan.
Delivery
The record of one firing: which event, which rule, every attempt, what your endpoint answered, and when the next retry is due.
Three requests to a working webhook
Register, attach, test
// 1. Register the destination once. Keep the signing secret —
// it is returned here and never again.
const { data: destination } = await call("/webhook-destinations", {
method: "POST",
body: JSON.stringify({
name: "Order automation",
url: "https://example.com/webhooks/countdownshare",
}),
});
await secrets.store("COUNTDOWNSHARE_WEBHOOK_SECRET", destination.signing_secret);
// 2. Attach a rule to the timer you care about.
await call(`/timers/${timerId}/webhook-rules`, {
method: "POST",
body: JSON.stringify({
name: "Reservation expired",
webhook_destination_id: destination.id,
condition: { field: "status", operator: "equals", value: "ended" },
delivery: "once",
}),
});
// 3. Confirm the plumbing before you rely on it.
await call(`/webhook-destinations/${destination.id}/test`, { method: "POST" });Step three is the one worth not skipping. A test delivery is a real signed request, so it proves your endpoint is reachable and your signature verification is correct before a real timer depends on it — which separates “my handler is broken” from “my rule never matched”.
What a handler must do
Four obligations, and all four have caught someone before.
- 01
Verify the signature against the raw body
Before parsing. A JSON round-trip changes whitespace and key order, and the check will fail. This is the single most common integration bug.
- 02
Answer within 10 seconds
Return any 2xx. Anything slower is a timeout and gets retried — so acknowledge first and queue the real work.
- 03
Tolerate duplicates
Delivery is at-least-once. The same event ID can arrive twice after a retry or a replay. Make the handler idempotent.
- 04
Use the right status code
A 2xx means "received". A 5xx or a timeout means "try again". A 4xx other than 408, 425, and 429 means "do not bother" and stops retries permanently.
Every webhook endpoint
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /webhook-destinations | List destinations |
| POST | /webhook-destinations | Register one — returns the signing secret once |
| GET | /webhook-destinations/{destination_id} | Get one, without its secret |
| PATCH | /webhook-destinations/{destination_id} | Change name, URL, description, or enabled state |
| DELETE | /webhook-destinations/{destination_id} | Delete one no rule depends on |
| POST | /webhook-destinations/{destination_id}/test | Send a signed test delivery |
| POST | /webhook-destinations/{destination_id}/rotate-secret | Replace the signing secret |
| GET | /timers/{timer_id}/webhook-rules | List rules on a timer |
| POST | /timers/{timer_id}/webhook-rules | Attach a rule |
| PATCH | /timers/{timer_id}/webhook-rules/{rule_id} | Change a condition, destination, or state |
| DELETE | /timers/{timer_id}/webhook-rules/{rule_id} | Remove a rule |
| GET | /webhook-deliveries | List deliveries, filterable by status |
| GET | /webhook-deliveries/{delivery_id} | Get one with its attempt history |
| POST | /webhook-deliveries/{delivery_id}/replay | Replay a delivery |
Read next
Setting up
Destinations — registering a URL, storing the secret, and rotating it without downtime. Rules — the six conditions you can watch and when to use repeat instead of once.
Receiving
Events and payload — every event type and the exact body your endpoint receives. Verify a delivery — working HMAC verification and the mistakes that break it.
When something goes wrong
Retries and replay — the retry schedule, delivery statuses, and how to resend after you have fixed the bug.