Skip to content

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"
Response
{
  "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":

FieldMeaning
occurrence.next_starts_atUTC instant of the next live occurrence, or null when the series has ended or no occurrence is known
occurrence.next_starts_localThe 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=false when you only want one kind.
  • Treat occurrence.next_starts_at: null as "nothing upcoming," not as an error — keep the listing's id/slug around if you've already published a link to it.
  • Don't try to reconstruct a full occurrence calendar from rrule and call it authoritative — occurrence.next_starts_at is the one the API itself computed and stands behind; a client-side RRULE expansion can drift from it (holidays, one-off cancellations, schedule changes).