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.

FieldTypeRequired forDescription
frequencystringAlldaily, weekly, monthly, or custom.
local_timestringAllHH:MM in 24-hour form, in the timer’s timezone. "09:30", not "9:30am".
days_of_weekarrayweeklyLowercase day names: ["monday", "thursday"]. At least one.
day_of_monthintegermonthly1 to 31.
rrulestringcustomAn RFC 5545 rule, up to 500 characters. DAILY, WEEKLY, and MONTHLY are supported.
intervalintegerNoRepeat 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

PartSupportedExample
FREQDAILY, WEEKLY, MONTHLYFREQ=WEEKLY
INTERVALYesINTERVAL=2 — every second period
BYDAYYes, with WEEKLY and MONTHLYBYDAY=MO,WE,FR or BYDAY=2TU (second Tuesday)
BYMONTHDAYYes, with MONTHLYBYMONTHDAY=-1 — the last day of the month
COUNT / UNTILYesCOUNT=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 wantrrule
Weekdays onlyFREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
The last Friday of each monthFREQ=MONTHLY;BYDAY=-1FR
The second Tuesday of each monthFREQ=MONTHLY;BYDAY=2TU
The last day of each monthFREQ=MONTHLY;BYMONTHDAY=-1
Every other week on MondayFREQ=WEEKLY;INTERVAL=2;BYDAY=MO
Ten daily occurrences, then stopFREQ=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.

This is what makes recurring timers cheap. One timer counting a daily cutoff for a year is one against your monthly allowance, not 365. Creating a fresh fixed timer each day would cost 365 and give you 365 URLs to manage.

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"
  }'
deliveryOn a recurring timerUse it for
onceFires on the first matching cycle, then never againA one-off announcement — the first time the schedule is reached.
repeatFires again on every subsequent cycleAlmost everything else: reminders, cutoff notifications, integrations.
A rule that fired once and appears broken is nearly always a 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.