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);
});
PollingWebhooks
Requests for 500 timers720,000 per day at 1-minute intervalsOne per event
How late you find outUp to your intervalSeconds
Rate limit pressureGrows with every timer addedIndependent of timer count
Works while your service is downNo — the poll simply does not happenYes — deliveries retry for up to about 15 hours
Polling still has a place: reading a countdown to render it is a read, and the status endpoint is the right tool. Use webhooks for reacting, and status for displaying.

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.

Destinations are limited by plan; rules are not. So the shape that scales is a small number of destinations — one per receiving service — with as many rules as you have timers and milestones. See pricing.

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.

  1. 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.

  2. 02

    Answer within 10 seconds

    Return any 2xx. Anything slower is a timeout and gets retried — so acknowledge first and queue the real work.

  3. 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.

  4. 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.

Returning a 4xx because you could not process the payload tells us to stop trying. If the failure is on your side and might clear — a database that is momentarily unavailable — return a 5xx so the delivery is retried. See retries and replay.

Every webhook endpoint

MethodEndpointPurpose
GET/webhook-destinationsList destinations
POST/webhook-destinationsRegister 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}/testSend a signed test delivery
POST/webhook-destinations/{destination_id}/rotate-secretReplace the signing secret
GET/timers/{timer_id}/webhook-rulesList rules on a timer
POST/timers/{timer_id}/webhook-rulesAttach 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-deliveriesList deliveries, filterable by status
GET/webhook-deliveries/{delivery_id}Get one with its attempt history
POST/webhook-deliveries/{delivery_id}/replayReplay 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.