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"
}'| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–100 characters. Internal label — it appears on delivery records, so make it descriptive. |
webhook_destination_id | string | Yes | The ID from POST or GET /webhook-destinations. |
condition.field | string | Yes | status, remaining_seconds, progress_percent, server_time, action, or recurrence_cycle. |
condition.operator | string | Yes | equals, less_than_or_equal, or greater_than_or_equal. |
condition.value | mixed | Yes | The value compared against the field. Type depends on the field. |
delivery | string | No | once or repeat. Defaults to once. |
enabled | boolean | No | Evaluate the rule when true. Defaults to true. |
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.
| field | Operator | Example value | Fires when |
|---|---|---|---|
status | equals | "ended" | The countdown reaches zero. The most common rule by a wide margin. |
remaining_seconds | less_than_or_equal | 3600 | An hour or less is left. Use this for reminders ahead of a deadline. |
progress_percent | greater_than_or_equal | 75 | Three quarters of the way through, whatever the total duration is. |
server_time | greater_than_or_equal | "2030-01-01T14:00:00Z" | A wall-clock moment passes, independent of the timer’s own deadline. |
action | equals | "paused" | Someone starts, pauses, resumes, or resets a duration timer. |
recurrence_cycle | equals | "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.
| delivery | Behaviour | Use for |
|---|---|---|
once | Fires 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. |
repeat | Becomes eligible again after a recurring countdown rolls into its next cycle. | Anything on a recurring timer that should fire every cycle. |
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.
data.remaining_seconds to thresholds a second time, and it stays correct if you change a threshold later.Managing rules
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /timers/{timer_id}/webhook-rules | List the rules on a timer |
| POST | /timers/{timer_id}/webhook-rules | Attach 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.
- 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.
- 02
The destination is disabled
A disabled destination accepts nothing. Read it back and check enabled.
- 03
The rule itself is disabled
Same check, on the rule.
- 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.
- 05
Deliveries are failing, not missing
GET /webhook-deliveries?status=failed. If they are there, this is a handler problem, not a rule problem.
- 06
The timer is in the other environment
Sandbox rules watch Sandbox timers only. Check which key created what.
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.