Find listings
Recurring events
How a weekly or monthly series is represented and how its next date is computed.
This page is for anyone building a calendar, feed, or newsletter view: it explains how a weekly storytime or a monthly meetup shows up in the API — one listing per series, not one row per Saturday — and what happens once a series has run its course.
curl -sS -H "X-API-Key: $TAILEND_API_KEY" --get https://api.tailend.dev/v1/philadelphia/events \
--data-urlencode "recurring=true" \
--data-urlencode "limit=1"const res = await fetch(
"https://api.tailend.dev/v1/philadelphia/events?recurring=true&limit=1",
{ headers: { "X-API-Key": process.env.TAILEND_API_KEY } },
);
const { data } = await res.json();
console.log(data[0].name, data[0].rrule, data[0].occurrence);{
"data": [
{
"id": "2f3c9d4e-3d2b-4b7d-9a3e-1b2c3d4e5f60",
"name": "Storytime in the Park",
"is_recurring": true,
"rrule": "FREQ=WEEKLY;BYDAY=SA",
"recurrence_description": "Every Saturday through October",
"starts_at": "2026-08-01T14:00:00Z",
"occurrence": {
"next_starts_at": "2026-09-19T14:00:00Z",
"next_starts_local": "2026-09-19T10:00:00"
}
}
]
}One listing per series
A recurring event is a single row, not one row per occurrence. is_recurring tells you it repeats, rrule gives the machine-readable recurrence rule (RFC 5545), and recurrence_description gives the human-readable version ("Every Saturday through October"). There's no separate endpoint or per-occurrence resource — a weekly storytime is one id, one slug, one permalink, for the life of the series.
How the next occurrence works in list results
starts_at/starts_local on a recurring row are the series' original anchor — when it first started, not necessarily anything upcoming. The occurrence block carries the one that matters for "what's happening":
| Field | Meaning |
|---|---|
occurrence.next_starts_at | UTC instant of the next live occurrence, or null when the series has ended or no occurrence is known |
occurrence.next_starts_local | The same instant, as local wall-clock time |
List results (GET /v1/{metro}/events) order and window recurring rows by this next occurrence rather than by the original anchor date — so a series that started in January and still meets every Saturday shows up in a from/to window that covers next Saturday, not just a window that covers January. A one-off (non-recurring) event keeps working exactly as before, windowed and ordered on its own starts_at.
occurrence is additive: check for its presence rather than assuming it's always there, and always check has_date/starts_at first for whether an event has a date at all (see the API reference).
Finished series
Once a recurring series has no more live occurrences, occurrence.next_starts_at is null and the row drops out of ordinary list results — there's nothing left to window it against. The listing itself isn't deleted: GET /v1/events/{id} and the slug route still resolve it (so a permalink you've already published or indexed keeps working), it's just not returned by a plain from/to list call once its last occurrence has passed.
What the API doesn't carry: per-occurrence exceptions
There's no structured list of skipped or moved dates (a 2026-09-27 occurrence cancelled, or moved to 2026-09-28) — the API carries the series' current rrule and next_starts_at/next_starts_local, which always reflect the source's current schedule, not a history of individual changes to it. If a specific occurrence date changed, that shows up as a new occurrence.next_starts_at value the next time you read the row — not as an exception record attached to the old date.
If you need to know when a listing's dates or price changed, rather than just what's current, GET /v1/listings/{id}/versions gives you that history — each entry has a changed_at, a kind (date_changed is the one you want), and the names of the fields that changed. See the API reference for the full shape, and Pagination for walking it.
What to do
- Filter with
recurring=true/recurring=falsewhen you only want one kind. - Treat
occurrence.next_starts_at: nullas "nothing upcoming," not as an error — keep the listing'sid/slugaround if you've already published a link to it. - Don't try to reconstruct a full occurrence calendar from
rruleand call it authoritative —occurrence.next_starts_atis the one the API itself computed and stands behind; a client-side RRULE expansion can drift from it (holidays, one-off cancellations, schedule changes).