Recurring countdown timers
A recurring timer counts down to the next occurrence of a schedule, and when that occurrence passes it rolls forward to the one after it — indefinitely, without you recreating anything. One timer, one ID, one URL that stays correct forever. It is also the only timer type where the timezone changes behaviour rather than just presentation, which is where most of the care on this page goes.
The recurrence object
Two fields are always required. The rest depend on which frequency you chose.
| Field | Type | Required for | Description |
|---|---|---|---|
frequency | string | All | daily, weekly, monthly, or custom. |
local_time | string | All | HH:MM in 24-hour form, in the timer’s timezone. "09:30", not "9:30am". |
days_of_week | array | weekly | Lowercase day names: ["monday", "thursday"]. At least one. |
day_of_month | integer | monthly | 1 to 31. |
rrule | string | custom | An RFC 5545 rule, up to 500 characters. DAILY, WEEKLY, and MONTHLY are supported. |
interval | integer | No | Repeat every N periods. 1 to 365. Defaults to 1. |
local_time is a wall-clock reading, not an instant — which is exactly why it needs a zone to become one. Send timezone alongside it. Omitting the timezone is legal and runs the schedule in UTC, which is almost never what anyone means.Daily
The simplest schedule: the same time every day. A daily order cutoff, a nightly deadline, a standup.
Every day at 15:00 Chicago time
{
"name": "Daily order cutoff",
"type": "recurring",
"recurrence": {
"frequency": "daily",
"local_time": "15:00"
},
"timezone": "America/Chicago"
}Every third day
{
"frequency": "daily",
"interval": 3, // every third day
"local_time": "08:00"
}interval works with every frequency. With daily it means every N days; with weekly, every N weeks; with monthly, every N months.
Weekly
One or more named days. Listing several days means the timer counts to whichever comes next — so the example below counts to Saturday 23:59, then to Sunday 23:59, then to the following Saturday.
Saturdays and Sundays at 23:59 London time
{
"name": "Weekend flash sale ends",
"type": "recurring",
"recurrence": {
"frequency": "weekly",
"days_of_week": ["saturday", "sunday"],
"local_time": "23:59"
},
"timezone": "Europe/London"
}Day names are lowercase and spelled out: monday through sunday. Abbreviations and numeric days are rejected.
Monthly
A fixed day of the month. Billing dates, reporting deadlines, rent reminders.
The 1st of every month at 09:00 IST
{
"name": "Invoice due",
"type": "recurring",
"recurrence": {
"frequency": "monthly",
"day_of_month": 1,
"local_time": "09:00"
},
"timezone": "Asia/Kolkata"
}day_of_month accepts 1 to 31, but not every month has 31 days. A schedule set to the 31st does not fire in February, and a schedule set to the 30th does not fire in February either. If you mean “the last day of the month”, use a custom rule with BYMONTHDAY=-1 rather than picking 31 and hoping.Custom RRULE schedules
When the three named frequencies do not fit, frequency: "custom" takes an RFC 5545 recurrence rule — the same format iCalendar uses, so anything your calendar can express is likely expressible here.
Every other Thursday at 14:00 Berlin time
{
"name": "Fortnightly sprint review",
"type": "recurring",
"recurrence": {
"frequency": "custom",
"rrule": "FREQ=WEEKLY;INTERVAL=2;BYDAY=TH",
"local_time": "14:00"
},
"timezone": "Europe/Berlin"
}The supported subset
| Part | Supported | Example |
|---|---|---|
FREQ | DAILY, WEEKLY, MONTHLY | FREQ=WEEKLY |
INTERVAL | Yes | INTERVAL=2 — every second period |
BYDAY | Yes, with WEEKLY and MONTHLY | BYDAY=MO,WE,FR or BYDAY=2TU (second Tuesday) |
BYMONTHDAY | Yes, with MONTHLY | BYMONTHDAY=-1 — the last day of the month |
COUNT / UNTIL | Yes | COUNT=10 — stop after ten occurrences |
FREQ=YEARLY, FREQ=HOURLY, and FREQ=MINUTELY are not supported. An annual event is better served by a fixed-date countdown you update once a year, and sub-daily repetition is what a duration timer with a reset is for. A rule outside the subset returns validation_error naming the unsupported part.Useful rules
| You want | rrule |
|---|---|
Weekdays only | FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR |
The last Friday of each month | FREQ=MONTHLY;BYDAY=-1FR |
The second Tuesday of each month | FREQ=MONTHLY;BYDAY=2TU |
The last day of each month | FREQ=MONTHLY;BYMONTHDAY=-1 |
Every other week on Monday | FREQ=WEEKLY;INTERVAL=2;BYDAY=MO |
Ten daily occurrences, then stop | FREQ=DAILY;COUNT=10 |
How a cycle rolls over
When an occurrence is reached, the timer emits timer.recurrence_completed and immediately begins counting to the next occurrence. The ID does not change, the public URL does not change, and nothing needs to be recreated.
Read the current cycle from the status endpoint — target_at holds the next occurrence, and remaining counts to it. A recurring timer is therefore almost never ended: it passes through zero and starts again.
Webhook rules on a repeating timer
This is where delivery earns its keep. A rule set to once fires a single time in the timer's entire life — which for a recurring timer means once, ever, and then never again. Set it to repeat and the rule becomes eligible again on each new cycle.
Fire two hours before every occurrence
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": "Two hours before cutoff",
"webhook_destination_id": "0f4726a4-c05b-4eba-97d2-30ba8e59446e",
"condition": {
"field": "remaining_seconds",
"operator": "less_than_or_equal",
"value": 7200
},
"delivery": "repeat"
}'| delivery | On a recurring timer | Use it for |
|---|---|---|
once | Fires on the first matching cycle, then never again | A one-off announcement — the first time the schedule is reached. |
repeat | Fires again on every subsequent cycle | Almost everything else: reminders, cutoff notifications, integrations. |
once rule on a recurring timer. Check that first. Full detail on webhook rules.Daylight saving
Occurrences are computed in the timer's IANA zone, so a 09:00 daily timer stays at 09:00 local through a DST transition instead of drifting to 08:00 or 10:00. That is the whole reason the API insists on a zone identifier and rejects a numeric offset — an offset cannot know when the rules change.
The two awkward hours still exist. If a schedule lands on a time that is skipped when clocks spring forward, that occurrence resolves to the adjacent valid instant; if it lands on an hour that occurs twice, it resolves once. Choosing a local_time outside 01:00–03:00 sidesteps both, and is worth doing if the exact minute does not matter. More under dates and timezones.
Changing or removing a schedule
recurrence is editable. Send a new object to replace the schedule — it is replaced wholesale, not merged, so include every field the frequency requires. Sending null removes the schedule.
Like every update, this needs the current revision in an If-Match header. A change takes effect from the next occurrence to be computed; it does not retroactively alter a cycle already in progress. See update and delete.