Webhook rules and conditions

A rule is one condition on one timer, pointing at one destination. When the condition becomes true we send a signed delivery. Rules are unlimited on every plan, so the right pattern is several small rules — one per moment you care about — rather than one rule doing several jobs.

Creating a rule

Rules live under the timer they watch, so the timer ID is in the path. The destination must already exist — create it first and keep its ID.

Fire one hour before the deadline

curl -X POST \
  https://countdownshare.com/api/v1/timers/$TIMER_ID/webhook-rules \
  -H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "One hour remaining",
    "webhook_destination_id": "0f4726a4-c05b-4eba-97d2-30ba8e59446e",
    "condition": {
      "field": "remaining_seconds",
      "operator": "less_than_or_equal",
      "value": 3600
    },
    "delivery": "once"
  }'
FieldTypeRequiredDescription
namestringYes1–100 characters. Internal label — it appears on delivery records, so make it descriptive.
webhook_destination_idstringYesThe ID from POST or GET /webhook-destinations.
condition.fieldstringYesstatus, remaining_seconds, progress_percent, server_time, action, or recurrence_cycle.
condition.operatorstringYesequals, less_than_or_equal, or greater_than_or_equal.
condition.valuemixedYesThe value compared against the field. Type depends on the field.
deliverystringNoonce or repeat. Defaults to once.
enabledbooleanNoEvaluate the rule when true. Defaults to true.
Name rules for the moment, not the mechanism. “One hour remaining” tells you what happened when you are reading a delivery record at 2am; “rule-3” does not.

The six condition fields

Each rule watches exactly one field. There is no AND or OR — combining conditions means creating more rules, which is why they are unlimited.

fieldOperatorExample valueFires when
statusequals"ended"The countdown reaches zero. The most common rule by a wide margin.
remaining_secondsless_than_or_equal3600An hour or less is left. Use this for reminders ahead of a deadline.
progress_percentgreater_than_or_equal75Three quarters of the way through, whatever the total duration is.
server_timegreater_than_or_equal"2030-01-01T14:00:00Z"A wall-clock moment passes, independent of the timer’s own deadline.
actionequals"paused"Someone starts, pauses, resumes, or resets a duration timer.
recurrence_cycleequals"completed"A recurring cycle finishes and the timer advances to the next occurrence.

Choosing between remaining_seconds and progress_percent

remaining_seconds is absolute and is what you want when the reminder has a real-world meaning — “one hour before the webinar” means one hour regardless of how long the countdown ran.

progress_percent is relative and is right when the same rule should apply across timers of very different lengths. Seventy-five percent through a fifteen-minute cart hold and seventy-five percent through a fourteen-day trial are both “most of the way there”, and one rule expresses both.

once versus repeat

This is the field most likely to produce behaviour you did not intend, and the direction of the mistake differs by timer type.

deliveryBehaviourUse for
onceFires a single time in the timer’s entire life, then never again.A milestone on a one-off countdown: ended, one hour left, 75% through.
repeatBecomes eligible again after a recurring countdown rolls into its next cycle.Anything on a recurring timer that should fire every cycle.
A once rule on a recurring timer fires on the first cycle and stays silent forever afterwards. That is the single most common “my webhook stopped working” report, and it is the first thing to check. See recurring timers.

On a non-recurring timer the distinction barely matters — there is no next cycle for a repeat rule to become eligible in — so leaving the default is fine.

Several rules on one timer

Because rules are unlimited and destinations are not, the shape that works is many rules pointing at a small number of endpoints. A reminder ladder is four rules and one destination.

A reminder ladder

// Several rules on one timer, all pointing at one destination.
// Each fires once, at its own point in the countdown.
const milestones = [
  { name: "24 hours left", value: 86_400 },
  { name: "1 hour left",   value: 3_600 },
  { name: "5 minutes left", value: 300 },
];

for (const milestone of milestones) {
  await call(`/timers/${timerId}/webhook-rules`, {
    method: "POST",
    body: JSON.stringify({
      name: milestone.name,
      webhook_destination_id: destinationId,
      condition: {
        field: "remaining_seconds",
        operator: "less_than_or_equal",
        value: milestone.value,
      },
      delivery: "once",
    }),
  });
}

// Plus the one that matters most.
await call(`/timers/${timerId}/webhook-rules`, {
  method: "POST",
  body: JSON.stringify({
    name: "Ended",
    webhook_destination_id: destinationId,
    condition: { field: "status", operator: "equals", value: "ended" },
    delivery: "once",
  }),
});

Each rule produces its own delivery, carrying its own rule.name, so your handler can branch on which milestone fired without inspecting the numbers itself.

Keep the rule names in your own code as constants and switch on them in the handler. It is more readable than comparing data.remaining_seconds to thresholds a second time, and it stays correct if you change a threshold later.

Managing rules

MethodEndpointPurpose
GET/timers/{timer_id}/webhook-rulesList the rules on a timer
POST/timers/{timer_id}/webhook-rulesAttach a rule
PATCH/timers/{timer_id}/webhook-rules/{rule_id}Change the condition, destination, or enabled state
DELETE/timers/{timer_id}/webhook-rules/{rule_id}Remove a rule

Setting enabled: false pauses a rule without losing its configuration — useful while you fix a handler, and better than deleting and recreating it. Rules are also removed automatically when their timer is deleted.

When a rule does not fire

Work through these in order. The first two account for most cases.

  1. 01

    It is a once rule that already fired

    Check GET /webhook-deliveries for this timer. If it fired on an earlier cycle, switch it to repeat.

  2. 02

    The destination is disabled

    A disabled destination accepts nothing. Read it back and check enabled.

  3. 03

    The rule itself is disabled

    Same check, on the rule.

  4. 04

    The condition was never true

    A remaining_seconds threshold larger than the timer’s total duration can never be reached. Sanity-check the number.

  5. 05

    Deliveries are failing, not missing

    GET /webhook-deliveries?status=failed. If they are there, this is a handler problem, not a rule problem.

  6. 06

    The timer is in the other environment

    Sandbox rules watch Sandbox timers only. Check which key created what.

The distinction between “never fired” and “fired and failed” is the fork in this diagnosis, and GET /webhook-deliveries answers it in one request. If a delivery record exists, the rule worked and the problem is downstream — see retries and replay.