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 sameidon every item.seriesId— the series this occurrence belongs to, the same value asidtoday.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
| Field | Value |
|---|---|
space | The space the event belongs to: id, did, domain, displayName, avatarUrl. Present on every endpoint here. |
title | The event's title. |
description | Plain text, or null. Free text exactly as the organizer wrote it. |
location | Free text exactly as the organizer wrote it. |
type | attend (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, null on an update event or when unspecified. Also an open enum. |
url | A link the organizer attached, or null. Arbitrary organizer-supplied content — treat it as untrusted. |
interestCount | How many members marked interest. Nothing on this API contributes to it — RSVP and interest are member actions in the app. |
requiresTicket | Whether attending requires buying a ticket. Always false on an update event, and independent of tickets. |
tickets | The tickets on sale that an anonymous caller can buy, in the order the organizer attached them (see Tickets). |
cover | The 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:
| Field | Value |
|---|---|
id | The store product id, the same one the products endpoints return. Stable; store this rather than the slug. |
slug | The mutable purchase-page alias. |
name | The ticket's name, e.g. General admission. |
prices | This 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. |
isSoldOut | True when this ticket had limited capacity and it is exhausted. Advisory only — availability is re-checked when a purchase is made. |
purchasable | Whether 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.