Skip to main content

Events

Events are dated entries on a space's calendar — a session, a call, a party, or a date a space has promised to publish something. Only events a space published to everyone appear in this API; member-only events, drafts, and cancelled events are absent. Two endpoints cover them:

  • GET /v1/spaces/{space}/events — one space's public events in a time window.
  • GET /v1/events/upcoming — public events across every publicly discoverable space.

Both return the same item shape, ordered earliest first and cursor-paginated. The per-space endpoint takes any space identifier and returns 404 Not Found when the space has no public profile. The cross-space feed has no not-found: a space that is not publicly discoverable simply never contributes to it, exactly as it never appears in GET /v1/spaces.

Occurrences, not events

The unit on this wire is an occurrence. A recurring event is stored once and expanded on read, so a weekly series contributes one item per week inside the window you asked for. Three fields carry that identity:

  • id — the event. A recurring event returns the same id on every item.
  • seriesId — the series this occurrence belongs to. The same value as id today; the pair is what stays correct if occurrences ever gain identities of their own.
  • recurrenceId — this occurrence's stable identity within its series: its local wall-clock time (2026-08-15T19:00:00) or, for an all-day event, its date (2026-08-15).

Address a specific occurrence by the (seriesId, recurrenceId) pair. Never identify one by its resolved UTC instant — daylight-saving shifts move the instant while the recurrence identity holds steady. These are the same occurrence identities the space's own calendar uses.

Time windows

Both endpoints take an optional from and to (ISO 8601):

curl 'https://api.dfos.com/v1/spaces/field-notes/events?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z'
  • from defaults to now.
  • to defaults to 90 days after from.
  • The widest window served is 366 days.

The window selects overlapping occurrences, not ones that start inside it — a multi-day festival already under way is still returned.

Windows are clamped, never rejected. A window longer than 366 days, or one that ends before it starts, silently becomes the widest window this API serves rather than returning an error. Read the times on the items you get back rather than assuming your requested window was honored verbatim.

Paging a window

Pagination follows the shared cursor contract — pass nextCursor back as after, keep limit constant, treat cursors as opaque. Hold from and to constant for the whole walk too: every page re-expands the same window.

Two things differ from the row-backed listings. A cursor walk reaches at most the first 500 occurrences of a window — narrow the window to see past them, rather than paging further. And these listings do not compute totalCount, so it is absent from the envelope.

Event fields

Each item carries:

  • space — the space the event belongs to: id, did, domain, displayName, and avatarUrl. Present on both endpoints, so a cross-space feed can attribute and link every item without a second request.
  • title, description (plain text or null), and location — free text exactly as the organizer wrote it.
  • typeattend (people show up) or update (the space will publish something on this date). An open enum; treat an unrecognized value as a generic event.
  • mode — how an attend event happens: call, place, or chat. Presentational only, and null on an update event or when unspecified. Also an open enum.
  • url — a link the organizer attached (a call link, a ticket page), or null. This is arbitrary organizer-supplied content: treat it as untrusted.
  • interestCount — how many members marked interest. This API is read-only; there is no anonymous RSVP, so a caller can never contribute to this number.
  • cover — the image to render for this occurrence, when it has one: the occurrence's own cover art if the organizer set one, otherwise the event's. Always a public image with a permanent, unsigned URL — no urlExpiresAt, and safe to hotlink while the event references it. This endpoint never emits a signed or expiring URL.

All-day versus timed

isAllDay selects which of two mutually exclusive time shapes an item uses. Every field of the other shape is null.

Timed (isAllDay: false):

  • startsAt / endsAt — this occurrence's resolved instants (ISO 8601 UTC). endsAt is null for an open-ended event and equal to startsAt for a point-in-time one.
  • localStart — the start as local wall-clock time (2026-08-15T19:00:00) in timeZone. This is the time the organizer actually authored, and it equals recurrenceId.
  • timeZone — the IANA zone the event was authored in, e.g. America/Chicago.

All-day (isAllDay: true):

  • startDateYYYY-MM-DD.
  • endDate — an exclusive end date, or null for a single day.

Render an all-day event from its dates, not by converting an instant: "7pm local, whatever UTC that lands on" is the real intent of a calendar entry, and the instant alone is lossy across a DST boundary.

What's not here

The recurrence rule itself, whether an occurrence has been fulfilled by a post, and the fulfilling post are all absent — an item describes one occurrence, not the series' schedule. Attendee lists and RSVP are member-only and never reach this API.

What to store

Store the (seriesId, recurrenceId) pair as an occurrence's durable key, and the space's id or did for attribution. Re-fetch cover URLs rather than archiving them (see Compatibility → Media URLs), and never store a resolved UTC instant as an occurrence's identity.