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 here; member-only events, drafts, and canceled events are absent.

  • GET /v1/spaces/{space}/events — one space's public events in a time window.
  • GET /v1/spaces/{space}/events/{eventId} — one event, resolved to a single occurrence.
  • GET /v1/events/upcoming — public events across every publicly discoverable space.

All three return the same item shape; the two listings are ordered earliest first and cursor-paginated. The per-space endpoints take any space identifier and return 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 never contributes to it, just as it never appears in GET /v1/spaces.

Occurrences, not events​

The unit on this wire is an occurrence: a recurring event contributes one item per occurrence 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.
  • 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 an occurrence by the (seriesId, recurrenceId) pair and store that pair as its durable key. Never identify one by its resolved UTC instant, which daylight saving moves.

Time windows​

Both listings take an optional from and to (ISO 8601); the single-event read takes neither.

curl 'https://api.dfos.com/v1/spaces/home/events?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z'

from defaults to now, to defaults to 90 days after from, and 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 returned.

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

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. A walk reaches at most the first 500 occurrences of a window, so narrow the window rather than paging past them, and these listings do not compute totalCount.

Fetching one event​

GET /v1/spaces/{space}/events/{eventId} returns a single event resolved to one occurrence, with the same fields the listings emit.

curl 'https://api.dfos.com/v1/spaces/home/events/evt_ehrrkh8thkkdh6c383zkf9'

By default it returns the soonest upcoming public occurrence, within the same 366-day look-ahead the listings serve. Pin a specific one with occ, passing a recurrenceId exactly as a listing emitted it:

curl 'https://api.dfos.com/v1/spaces/home/events/evt_ehrrkh8thkkdh6c383zkf9?occ=2026-09-09'

occ is also how you reach an occurrence beyond that look-ahead: anchor a listing far in the future (?from=2029-01-01T00:00:00Z) and hand the recurrenceId it emits back here. Without a pin, an event whose only occurrence is past the horizon is absent.

Never construct an occ value yourself: one off by a second, or by a daylight-saving hour, does not name an occurrence. Every pin that does not name a live occurrence falls back to the soonest upcoming one rather than failing — an omitted, empty, or malformed value, a date that has passed, a recurrence rule the organizer changed, or a recurrenceId belonging to a different event. Read the recurrenceId on the response to know which occurrence you got.

The response is an identical 404 Not Found for every reason the event isn't publicly visible under that space: missing, private, draft, canceled, addressed under a different space, or with no upcoming public occurrence.

Event fields​

FieldValue
spaceThe space the event belongs to: id, did, domain, displayName, avatarUrl. Present on every endpoint here.
titleThe event's title.
descriptionPlain text, or null. Free text exactly as the organizer wrote it.
locationFree 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.
modeHow an attend event happens: call, place, or chat. Presentational only, null on an update event or when unspecified. Also an open enum.
urlA link the organizer attached, or null. Arbitrary organizer-supplied content — treat it as untrusted.
interestCountHow many members marked interest. Nothing on this API contributes to it — RSVP and interest are member actions in the app.
requiresTicketWhether attending requires buying a ticket. Always false on an update event, and independent of tickets.
ticketsThe tickets on sale that an anonymous caller can buy, in the order the organizer attached them (see Tickets).
coverThe occurrence's own cover art when the organizer set one, otherwise the event's. Always a public image with a permanent, unsigned URL and no urlExpiresAt.

cover is 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), where 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, the time the organizer authored, which equals recurrenceId; and timeZone, the IANA zone the event was authored in, e.g. America/Chicago.

All-day (isAllDay: true): startDate (YYYY-MM-DD) and endDate, an exclusive end date or null for a single day. Render an all-day event from its dates rather than by converting an instant, which is lossy across a DST boundary.

Tickets​

A ticket is an ordinary store product that admits to the event, so its slug goes straight to GET /v1/spaces/{space}/products/{slug} for the full purchase page. Each entry in tickets carries:

FieldValue
idThe store product id, the same one the products endpoints return. Stable; store this rather than the slug.
slugThe mutable purchase-page alias.
nameThe ticket's name, e.g. General admission.
pricesThis ticket's active tiers — the identical shape and selection the products endpoints emit for the same product, archived tiers never included. One-time and pay-what-you-want only.
isSoldOutTrue when this ticket had limited capacity and it is exhausted. Advisory only — availability is re-checked when a purchase is made.
purchasableWhether a purchase can be started right now. False when no tier a checkout could complete remains; render an unavailable state rather than a dead buy button. Independent of isSoldOut.

tickets is empty when the event sells none, and equally empty when it sells them only through pages that are not public: a ticket whose seller has not published a public purchase page never appears, and that product is a 404 on every other endpoint here. That is why an event can report requiresTicket: true alongside an empty tickets. Like everything else here, tickets says what is on sale, never what you already hold.

What's not here​

The recurrence rule, whether an occurrence has been fulfilled by a post, and the fulfilling post are absent — an item describes one occurrence, not the series' schedule. Attendee lists and RSVP are member-only and never reach this API. Tickets are listed but never sold here; the buy flow lives on the space's own public site.