openapi: 3.1.0 info: title: OpenAgenda API version: '3.0.0-draft' description: | The OpenAgenda v3 API gives programmatic access to agendas, events and locations. This contract is the single source of truth behind the generated SDK, the MCP server and this reference. ## Authentication Every request is authenticated with `Authorization: Bearer `, using one of two credentials, unless its operation declares another credential or none in its own security requirements: - **API key** — a read-only public key (`oa_pk_…`), which carries no user identity and reads what is public, or a secret key (`oa_sk_…`) for reads and writes. Best for server-to-server and scripts. - **OAuth 2.1** — authorization-code flow with mandatory PKCE (`S256`) for interactive apps acting on a user's behalf. The issued short-lived token carries the granted scopes (`agendas:read`, `events:read`, …). Public clients hold no secret. Use the **Authentication** selector at the top of this page to try requests with any of these credentials directly from the browser. ## Pagination List endpoints are **cursor-paginated**. Read `pagination.after` from a response and pass it back as the `after` query parameter to fetch the next page, with the same filters. ## Responses A field a response schema marks required is always present unless `fields` narrows the response: an empty array is `[]`, an empty localized map is `{}`, and an absent value is `null`. ## SDK & MCP - **TypeScript SDK** — [`@openagenda/api-client`](https://www.npmjs.com/package/@openagenda/api-client), generated from this contract. - **MCP server** — [`mcp.openagenda.com`](https://mcp.openagenda.com) exposes the same surface to AI agents over the Model Context Protocol. contact: name: OpenAgenda Support email: support@openagenda.com url: https://developers.openagenda.com license: name: Proprietary — © OpenAgenda identifier: LicenseRef-OpenAgenda-Proprietary # SDK-first story: the code samples on each operation use @openagenda/api-client # (generated from this very contract). Surface its install up front. x-scalar-sdk-installation: - lang: TypeScript description: | Install the official, fully-typed TypeScript SDK, generated from this contract: ```sh npm install @openagenda/api-client ``` Every operation's code sample uses it. servers: - url: https://api.openagenda.com/v3 description: Production - url: https://dapi.openagenda.com/v3 description: Development tags: - name: Agendas description: > Agendas hold and publish events. List and inspect them, then drill into the events they publish. - name: Events description: Events published in an agenda. - name: Locations description: > Where events happen. Each agenda has its own pool of locations — or shares one with other agendas through a location set. - name: Me description: > The authenticated caller: the agendas they are a member of and their role on each. Requires a user identity — a secret key or an OAuth token; a public (`oa_pk_…`) key carries no identity and answers `401`. - name: Keys description: > The API keys you hold, and what each one can do. - name: Uploads description: > Stage a media file (the native event image, or a custom file/image field) as `multipart/form-data`, then attach it by reference on an event write — so the media lands in the same write as the rest of the edit. The API validates the file's real type and size before storing it. # Groups the resource tags under a "Resources" heading in the Scalar sidebar. # Forward-compatible with the upcoming members surface (see scopes). x-tagGroups: - name: Resources tags: - Agendas - Events - Locations - Me - Keys - Uploads # Authenticate every request with either an API key or an OAuth2 token, # both carried as `Authorization: Bearer `. security: - oauth2: - events:read - secretKey: [] - publicKey: [] paths: /agendas: get: operationId: agendas.list tags: [Agendas] summary: List agendas x-synonyms: [calendars, directory, catalogue, catalog, browse, find, all] x-codeSamples: - lang: TypeScript label: List official agendas source: | const { data, error } = await oa.agendas.list({ query: { official: true, limit: 20 }, }); if (error) throw error; return data.data.map((a) => ({ uid: a.uid, title: a.title })); description: > Returns a cursor-paginated list of agendas. Pass the `after` cursor returned in `pagination.after` to fetch the next page, with the same filters. Private agendas and agendas not indexed in public search are absent from this list. Unknown or malformed filter values answer `400`, with per-field context under `error.errors`. An unrecognized top-level query parameter is ignored. security: - publicKey: [] - secretKey: [] - oauth2: - agendas:read parameters: - $ref: '#/components/parameters/AfterCursor' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Detailed' - $ref: '#/components/parameters/Fields' - $ref: '#/components/parameters/AgendaSearch' - $ref: '#/components/parameters/AgendaSort' - $ref: '#/components/parameters/AgendaFilterUid' - $ref: '#/components/parameters/AgendaFilterSlug' - $ref: '#/components/parameters/AgendaFilterOfficial' responses: '200': description: A page of agendas. content: application/json: schema: $ref: '#/components/schemas/AgendaList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /agendas/{agendaUid}: get: operationId: agendas.get tags: [Agendas] summary: Get one agenda x-synonyms: [calendar, detail, single, one, by id, uid, fetch, retrieve, lookup] x-codeSamples: - lang: TypeScript label: Get one agenda by uid source: | const { data, error } = await oa.agendas.get({ path: { agendaUid: 12345 }, }); if (error) throw error; return data; description: > Returns a single agenda in its full `Agenda` representation. An unknown agenda uid answers `404`. security: - publicKey: [] - secretKey: [] - oauth2: - agendas:read parameters: - $ref: '#/components/parameters/AgendaUid' responses: '200': description: The agenda. content: application/json: schema: $ref: '#/components/schemas/Agenda' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/overview: get: operationId: agendas.overview tags: [Agendas] summary: Get an agenda's activity overview x-synonyms: [summary, stats, statistics, metrics, counts, activity, dashboard, kpi] x-codeSamples: - lang: TypeScript label: Overview of one agenda source: | const { data, error } = await oa.agendas.overview({ path: { agendaUid: 12345 }, }); if (error) throw error; return data; description: > Returns a live activity overview of the agenda: event volume, distributions (by language, source, end-day…), spatial viewport and thematic keywords. Two visibility scopes. `events.published` (publicly visible events) is returned to every caller. `events.all` (events of any moderation state, moderation/refused included) is returned ONLY to `administrator|moderator|internal` callers and is absent otherwise. Computed on each request. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' responses: '200': description: The agenda overview. content: application/json: schema: $ref: '#/components/schemas/AgendaOverview' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/events: get: operationId: agendas.events.list tags: [Events] summary: List events of an agenda x-synonyms: [find, browse, filter, paginate, cursor, upcoming, when, where, date, relevance] x-codeSamples: - lang: TypeScript label: All upcoming events in Paris (paging every result) source: | const events = []; let after; do { const { data, error } = await oa.agendas.events.list({ path: { agendaUid: 12345 }, query: { city: ['Paris'], relative: ['upcoming'], limit: 100, after }, }); if (error) throw error; events.push(...data.data); after = data.pagination.after; } while (after); return events.length; description: > Returns a cursor-paginated list of events for the given agenda. Pass the `after` cursor returned in `pagination.after` to fetch the next page, with the same filters. Only published events are listed. Unknown or malformed filter values answer `400`, with per-field context under `error.errors`. An unrecognized top-level query parameter is ignored. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/AfterCursor' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Detailed' - $ref: '#/components/parameters/Fields' - $ref: '#/components/parameters/Sort' # text & identity - $ref: '#/components/parameters/Search' - $ref: '#/components/parameters/RelevanceThreshold' - $ref: '#/components/parameters/FilterUid' - $ref: '#/components/parameters/FilterSlug' - $ref: '#/components/parameters/FilterExtId' # classification - $ref: '#/components/parameters/FilterKeyword' - $ref: '#/components/parameters/FilterLanguage' - $ref: '#/components/parameters/FilterAccessibility' - $ref: '#/components/parameters/FilterStatus' - $ref: '#/components/parameters/FilterAttendanceMode' - $ref: '#/components/parameters/FilterFeatured' # place - $ref: '#/components/parameters/FilterLocationUid' - $ref: '#/components/parameters/FilterLocationExtId' - $ref: '#/components/parameters/FilterRegion' - $ref: '#/components/parameters/FilterDepartment' - $ref: '#/components/parameters/FilterCity' - $ref: '#/components/parameters/FilterDistrict' - $ref: '#/components/parameters/FilterAdminLevel3' - $ref: '#/components/parameters/FilterAdminLevel5' - $ref: '#/components/parameters/FilterCountryCode' - $ref: '#/components/parameters/FilterBoundingBox' - $ref: '#/components/parameters/FilterNear' - $ref: '#/components/parameters/FilterRadius' # provenance - $ref: '#/components/parameters/FilterOriginAgendaUid' - $ref: '#/components/parameters/FilterOriginAgendaOfficial' - $ref: '#/components/parameters/FilterSourceAgendaUid' # time - $ref: '#/components/parameters/FilterRelative' - $ref: '#/components/parameters/FilterTimings' - $ref: '#/components/parameters/FilterLocalTime' - $ref: '#/components/parameters/FilterCreatedAt' - $ref: '#/components/parameters/FilterUpdatedAt' - $ref: '#/components/parameters/FilterAvailabilityUpdatedAt' # audience & additional fields - $ref: '#/components/parameters/FilterAge' - $ref: '#/components/parameters/FilterAdditionalFields' responses: '200': description: A page of events. content: application/json: schema: $ref: '#/components/schemas/EventList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' post: operationId: agendas.events.create tags: [Events] summary: Create an event x-synonyms: [add, new, create, publish, post, submit, insert] x-codeSamples: - lang: TypeScript label: Create an event with an agenda-specific additional field source: | const { data, error } = await oa.agendas.events.create({ path: { agendaUid: 12345 }, body: { title: { fr: 'Concert de jazz', en: 'Jazz concert' }, description: { fr: 'Une soirée jazz.', en: 'A jazz evening.' }, timings: [ { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }, ], location: { uid: 55512 }, additionalFields: { myCustomField: 'value' }, }, }); if (error) throw error; return data; description: > Creates an event in the given agenda and returns the created `Event`, with a `Location` header pointing at its canonical by-uid URL. Native writable fields sit at the top level; agenda-specific fields go under `additionalFields`; any other top-level key is rejected with `400`. The moderation `state` of the created event is arbitrated by the caller's role and the agenda's contribution settings - a contributor's event may be created pending moderation rather than published. The acting member must be allowed to create events in this agenda, otherwise `403`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventInput' responses: '201': description: The created event. headers: Location: description: Canonical by-uid URL of the created event. required: true schema: type: string example: /agendas/12345/events/9876543 content: application/json: schema: $ref: '#/components/schemas/Event' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' /agendas/{agendaUid}/events/validate: post: operationId: agendas.events.validate tags: [Events] summary: Validate an event without creating it x-synonyms: [dry run, check, validate, preview, lint, verify] x-codeSamples: - lang: TypeScript label: Dry-run validate an event body before creating it source: | const { data, error } = await oa.agendas.events.validate({ path: { agendaUid: 12345 }, body: { title: { fr: 'Concert de jazz' }, description: { fr: 'Une soirée jazz.' }, timings: [ { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }, ], location: { uid: 55512 }, }, }); if (error) throw error; // 422 with error.errors when invalid return data.valid; // true description: > Validates the body exactly as create does, without persisting anything. A well-formed, valid body answers `200 { "valid": true }`; invalid field values answer `422` with per-field problems under `error.errors[]`. Structural body problems (an unknown top-level key, an `additionalFields` name colliding with a native field) answer `400`. A native `image: { url }` is checked for URL syntax only (an `http(s)` URL without embedded credentials): the URL is not fetched, so a URL that create rejects with `422` at fetch time (unreachable host, private address, over 20 MiB, not an image) passes validate. The image counts as present, so an agenda whose schema requires one validates as it would create. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventInput' responses: '200': description: The body is valid. content: application/json: schema: $ref: '#/components/schemas/ValidationVerdict' example: valid: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' /agendas/{agendaUid}/events/{eventUid}: get: operationId: agendas.events.get tags: [Events] summary: Get a single event x-synonyms: [detail, single, one, by id, uid, fetch, retrieve, lookup] x-codeSamples: - lang: TypeScript label: Get a single event within an agenda source: | const { data, error } = await oa.agendas.events.get({ path: { agendaUid: 12345, eventUid: 67890 }, }); if (error) throw error; return data; description: > Returns a single event by its uid within the given agenda. An unknown event uid answers `404`. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/EventUid' responses: '200': description: The event. content: application/json: schema: $ref: '#/components/schemas/Event' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: operationId: agendas.events.update tags: [Events] summary: Replace an event x-synonyms: [update, replace, edit, put, overwrite, modify] x-codeSamples: - lang: TypeScript label: Replace an event's content source: | const { data, error } = await oa.agendas.events.update({ path: { agendaUid: 12345, eventUid: 67890 }, body: { title: { fr: 'Concert de jazz', en: 'Jazz concert' }, description: { fr: 'Une soirée jazz.', en: 'A jazz evening.' }, timings: [ { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }, ], location: { uid: 55512 }, }, }); if (error) throw error; return data; description: > Replaces the event's content with the submitted body and returns the updated `Event`. A full write: the server-side requirements of create apply. Use `PATCH` to change only a few fields. External id mappings (`extIds`) in the body are merged by key with the stored ones; pass `?mergeExtIds=false` to replace them wholesale. The acting member must be allowed to edit this event's content, otherwise `403`. An unknown event uid answers `404`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/EventUid' - $ref: '#/components/parameters/MergeExtIds' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventInput' responses: '200': description: The updated event. content: application/json: schema: $ref: '#/components/schemas/Event' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' patch: operationId: agendas.events.patch tags: [Events] summary: Partially update an event x-synonyms: [patch, partial update, edit, modify, change] x-codeSamples: - lang: TypeScript label: Change just the event's title source: | const { data, error } = await oa.agendas.events.patch({ path: { agendaUid: 12345, eventUid: 67890 }, body: { title: { fr: 'Nouveau titre', en: 'New title' } }, }); if (error) throw error; return data; description: > Updates only the fields present in the body (an `EventPatch`) and returns the updated `Event`. Fields left out are unchanged; `extIds` present in the body are merged by key with the stored ones. The acting member must be allowed to edit this event's content, otherwise `403`. An unknown event uid answers `404`, invalid field values `422`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/EventUid' - $ref: '#/components/parameters/MergeExtIds' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventPatch' responses: '200': description: The updated event. content: application/json: schema: $ref: '#/components/schemas/Event' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' delete: operationId: agendas.events.delete tags: [Events] summary: Delete an event x-synonyms: [delete, remove, destroy, cancel, drop] x-codeSamples: - lang: TypeScript label: Delete an event source: | const { data, error } = await oa.agendas.events.delete({ path: { agendaUid: 12345, eventUid: 67890 }, }); if (error) throw error; return data.deleted; // true description: > Removes the event from this agenda. When the agenda is the event's origin this deletes the event; otherwise it de-references it from this agenda (the origin keeps it). Answers `200` with a `DeletionResult` carrying the removed event's uid. The acting member must be allowed to remove the event, otherwise `403`. An unknown event uid answers `404`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/EventUid' responses: '200': description: The event was removed from this agenda. content: application/json: schema: $ref: '#/components/schemas/DeletionResult' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/events/ext/{extKey}/{extId}: get: operationId: agendas.events.getByExtId tags: [Events] summary: Get a single event by external id x-synonyms: [ext, external id, by ext, sync, source id, lookup, resolve] x-codeSamples: - lang: TypeScript label: Resolve an event by the id it carries in a sync source source: | const { data, error } = await oa.agendas.events.getByExtId({ path: { agendaUid: 12345, extKey: 'my-system', extId: 'abc-123' }, }); if (error) throw error; return data; description: > Returns a single event by its external identifier within the given agenda - the `(key, value)` pair an `ExtId` mapping carries in the event's `extIds`. Use this when you sync from your own system and hold its id rather than the OpenAgenda uid. Resolves to the same `Event` as the by-uid get; an unknown pair answers `404`. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/ExtKey' - $ref: '#/components/parameters/ExtIdValue' responses: '200': description: The event. content: application/json: schema: $ref: '#/components/schemas/Event' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: operationId: agendas.events.setByExtId tags: [Events] summary: Upsert an event by external id (replace) x-synonyms: [upsert, sync, set, put, replace, import, by ext] x-codeSamples: - lang: TypeScript label: Upsert an event addressed by its id in a sync source source: | const { data, error } = await oa.agendas.events.setByExtId({ path: { agendaUid: 12345, extKey: 'my-system', extId: 'abc-123' }, body: { title: { fr: 'Concert de jazz', en: 'Jazz concert' }, description: { fr: 'Une soirée jazz.', en: 'A jazz evening.' }, timings: [ { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }, ], location: { uid: 55512 }, }, }); if (error) throw error; return data; description: > Upserts the event carrying the `(extKey, extId)` pair: its content is REPLACED if it already exists (`200`), or the event is CREATED if not (`201` + a `Location` header pointing at its canonical by-uid URL). The path pair is authoritative - it is added to the event's `extIds`, keeping the mappings from other systems, so the event always carries its own external identity; any `extIds` in the body are ignored. This is the recommended write path for syncing from your own system: it is idempotent by external identity, so a lost response can be retried without creating a duplicate. The acting member must be allowed to create or edit the event, otherwise `403`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/ExtKey' - $ref: '#/components/parameters/ExtIdValue' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventInput' responses: '200': description: The existing event was updated. content: application/json: schema: $ref: '#/components/schemas/Event' '201': description: A new event was created. headers: Location: description: Canonical by-uid URL of the created event. required: true schema: type: string example: /agendas/12345/events/9876543 content: application/json: schema: $ref: '#/components/schemas/Event' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' patch: operationId: agendas.events.patchByExtId tags: [Events] summary: Upsert an event by external id (partial) x-synonyms: [upsert, sync, patch, partial, merge, by ext] x-codeSamples: - lang: TypeScript label: Partially upsert an event addressed by its sync id source: | const { data, error } = await oa.agendas.events.patchByExtId({ path: { agendaUid: 12345, extKey: 'my-system', extId: 'abc-123' }, body: { title: { fr: 'Nouveau titre', en: 'New title' } }, }); if (error) throw error; return data; description: > Upserts the event carrying the `(extKey, extId)` pair. When it already exists, only the fields present in the body (an `EventPatch`) are changed - the rest is left untouched (`200`). If no event carries the pair it is created (`201` + a `Location` header pointing at its canonical by-uid URL), in which case the full server-side requirements apply, so the body must be complete enough to create a valid event (else `422`). The path pair is added to the event's `extIds`, keeping the mappings from other systems; any `extIds` in the body are ignored. The acting member must be allowed to create or edit the event, otherwise `403`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/ExtKey' - $ref: '#/components/parameters/ExtIdValue' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EventPatch' responses: '200': description: The existing event was updated. content: application/json: schema: $ref: '#/components/schemas/Event' '201': description: A new event was created. headers: Location: description: Canonical by-uid URL of the created event. required: true schema: type: string example: /agendas/12345/events/9876543 content: application/json: schema: $ref: '#/components/schemas/Event' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' delete: operationId: agendas.events.deleteByExtId tags: [Events] summary: Delete an event by external id x-synonyms: [delete, remove, destroy, by ext, sync] x-codeSamples: - lang: TypeScript label: Delete an event addressed by its sync id source: | const { data, error } = await oa.agendas.events.deleteByExtId({ path: { agendaUid: 12345, extKey: 'my-system', extId: 'abc-123' }, }); if (error) throw error; return data.deleted; // true description: > Resolves the event carrying the `(extKey, extId)` pair and removes it. When the agenda is the event's origin this deletes the event; otherwise it de-references it from this agenda (the origin keeps it). Answers `200` with a `DeletionResult` carrying the resolved uid. The acting member must be allowed to remove the event, otherwise `403`. An unknown pair answers `404`. security: - secretKey: [] - oauth2: - events:write parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/ExtKey' - $ref: '#/components/parameters/ExtIdValue' responses: '200': description: The event was removed from this agenda. content: application/json: schema: $ref: '#/components/schemas/DeletionResult' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/events/facets: get: operationId: agendas.events.facets tags: [Events] summary: Faceted counts for an agenda's events — native facets plus its `additionalFields` x-synonyms: [ aggregate, count, group, group by, stats, statistics, histogram, distribution, how many, breakdown, top, ] x-codeSamples: - lang: TypeScript label: Stats overview of an agenda source: | const { data, error } = await oa.agendas.events.facets({ path: { agendaUid: 12345 }, query: { facets: ['cities', 'keywords', 'timespan', 'additionalFields', 'additionalFieldMetrics'], }, }); if (error) throw error; return data.facets; - lang: TypeScript label: How many concert events per city source: | const { data, error } = await oa.agendas.events.facets({ path: { agendaUid: 12345 }, query: { facets: ['cities'], keyword: ['concert'] }, }); if (error) throw error; return data.facets.cities; description: > Returns event counts grouped by the requested facets, computed over the **same filtered set** as `agendas.events.list`. Pass the same filter parameters to scope the counts (e.g. `?keyword=concert` then facet on `cities`). The response carries only the requested facets, in the shapes `FacetResults` defines. The agenda's own fields (its categories, labels…) are faceted by `additionalFields` - request it too for an agenda-wide stats overview (`additionalFieldMetrics` covers its numeric fields). The fields themselves are declared by the agenda's event form schema (`GET /agendas/{agendaUid}/events/schema`). Only published events are counted. `facets` selects which facets to compute; an unknown facet name returns `400`. Facets cover the whole filtered set in one response. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/Facets' - $ref: '#/components/parameters/FacetSize' - $ref: '#/components/parameters/FacetSizes' - $ref: '#/components/parameters/FacetSort' - $ref: '#/components/parameters/FacetSorts' - $ref: '#/components/parameters/FacetMissing' - $ref: '#/components/parameters/GeohashZoom' - $ref: '#/components/parameters/TimingsInterval' - $ref: '#/components/parameters/Month' - $ref: '#/components/parameters/AdditionalFieldsKeys' - $ref: '#/components/parameters/AdditionalFieldMetricsKeys' # Same filters as agendas.events.list — facets reflect the filtered set. - $ref: '#/components/parameters/Search' - $ref: '#/components/parameters/RelevanceThreshold' - $ref: '#/components/parameters/FilterUid' - $ref: '#/components/parameters/FilterSlug' - $ref: '#/components/parameters/FilterExtId' - $ref: '#/components/parameters/FilterKeyword' - $ref: '#/components/parameters/FilterLanguage' - $ref: '#/components/parameters/FilterAccessibility' - $ref: '#/components/parameters/FilterStatus' - $ref: '#/components/parameters/FilterAttendanceMode' - $ref: '#/components/parameters/FilterFeatured' - $ref: '#/components/parameters/FilterLocationUid' - $ref: '#/components/parameters/FilterLocationExtId' - $ref: '#/components/parameters/FilterRegion' - $ref: '#/components/parameters/FilterDepartment' - $ref: '#/components/parameters/FilterCity' - $ref: '#/components/parameters/FilterDistrict' - $ref: '#/components/parameters/FilterAdminLevel3' - $ref: '#/components/parameters/FilterAdminLevel5' - $ref: '#/components/parameters/FilterCountryCode' - $ref: '#/components/parameters/FilterBoundingBox' - $ref: '#/components/parameters/FilterNear' - $ref: '#/components/parameters/FilterRadius' - $ref: '#/components/parameters/FilterOriginAgendaUid' - $ref: '#/components/parameters/FilterOriginAgendaOfficial' - $ref: '#/components/parameters/FilterSourceAgendaUid' - $ref: '#/components/parameters/FilterRelative' - $ref: '#/components/parameters/FilterTimings' - $ref: '#/components/parameters/FilterLocalTime' - $ref: '#/components/parameters/FilterCreatedAt' - $ref: '#/components/parameters/FilterUpdatedAt' - $ref: '#/components/parameters/FilterAvailabilityUpdatedAt' - $ref: '#/components/parameters/FilterAge' - $ref: '#/components/parameters/FilterAdditionalFields' responses: '200': description: Facet counts over the filtered set. content: application/json: schema: $ref: '#/components/schemas/FacetResults' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' post: operationId: agendas.events.facetsReport tags: [Events] summary: Named, repeatable facet aggregations for an agenda's events x-synonyms: [aggregate, stats, statistics, dashboard, report, group by, breakdown] description: > Takes a JSON body of **named, repeatable** aggregations: the same facet `type` may appear several times under distinct `name` aliases, so a field can be aggregated several ways in one request (e.g. `timings` by `month` AND by `year`). Filters live under `filters` (an `EventFilters` object) and scope every facet. Each facet item is either a bare facet name or a `FacetSpec` object carrying per-instance options; `facetSize`/`facetSort` at the top level set the request-wide defaults an item overrides. The response is keyed by alias, each entry tagged with its `type` so the value shape is unambiguous. Only published events are counted. security: - publicKey: [] - secretKey: [] - oauth2: - events:read parameters: - $ref: '#/components/parameters/AgendaUid' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FacetReportRequest' responses: '200': description: Named facet results over the filtered set. content: application/json: schema: $ref: '#/components/schemas/FacetReport' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/events/schema: get: operationId: agendas.events.schema tags: [Events] summary: Event form schema of an agenda x-synonyms: [ form, fields, additional fields, custom fields, required, validate, structure, topology, definition, ] x-codeSamples: - lang: TypeScript label: Find the required fields, build a body, then dry-run it source: | const agendaUid = 12345; const { data: schema, error } = await oa.agendas.events.schema({ path: { agendaUid }, }); if (error) throw error; // What the agenda requires, and where each value goes: `when` // (enableWith) must hold for the requirement to apply, `unless` // (optionalWith) lifts it. const required = schema.fields .filter( (f) => f.path && (f.optional === false || (f.optional === undefined && f.optionalWith)), ) .map((f) => ({ path: f.path, when: f.enableWith ?? null, unless: f.optionalWith ?? null, })); const body = { title: { fr: 'Concert en ligne' }, description: { fr: 'Une soirée jazz diffusée en direct.' }, timings: [ { begin: '2030-06-20T18:00:00+02:00', end: '2030-06-20T22:00:00+02:00' }, ], attendanceMode: 2, // online: activates onlineAccessLink onlineAccessLink: 'https://example.com/live', }; const set = (path, value) => { const [key, sub] = path.split('.'); body[key] = sub ? { ...body[key], [sub]: value } : value; }; // A radio field takes the id of the option that fits the event. const theme = schema.fields.find((f) => f.field === 'thematique'); const concert = theme?.options.find((o) => o.value === 'concert'); if (concert) set(theme.path, concert.id); const check = await oa.agendas.events.validate({ path: { agendaUid }, body, }); // A 422 lists under `errors` each field to fix. return { required, verdict: check.error ? check.error.error : check.data }; - lang: TypeScript label: List the agenda's additional event fields source: | const { data, error } = await oa.agendas.events.schema({ path: { agendaUid: 12345 }, }); if (error) throw error; return data.fields .filter((f) => f.path?.startsWith('additionalFields.')) .map((f) => ({ name: f.field, type: f.fieldType })); description: > Returns the agenda's **merged event form schema** — the dynamic contract the events of this agenda follow. It merges the platform's native event fields with the network's and the agenda's own declarations: an agenda can add its own additional fields and override natives (make one required, restrict its options, relabel it). This is the same schema the OpenAgenda UI uses to build the event form: use it to know which fields exist, which are required and what their options are - e.g. to build payloads for the event write operations, to interpret `additionalFields` on events, or to discover the agenda-specific facets of the facets endpoint. Descriptors are scoped to your read access: a field whose `read` access levels exclude the caller (e.g. a moderator-only field) is omitted from `fields`, so a public caller sees only the public fields. Native fields the write operations do not take (the event's `uid`, `slug`, timestamps...) are omitted too, and `status` is listed only on an agenda that accepts it. security: - publicKey: [] - secretKey: [] - oauth2: - agendas:read parameters: - $ref: '#/components/parameters/AgendaUid' responses: '200': description: The merged event form schema. content: application/json: schema: $ref: '#/components/schemas/EventFormSchema' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /me/agendas: get: operationId: me.agendas.list tags: [Me] summary: List the agendas you are a member of x-synonyms: [ my agendas, memberships, my role, mine, which agendas, can I, permissions, by slug, my agenda, ] x-codeSamples: - lang: TypeScript label: Find one of my agendas by its slug source: | const { data, error } = await oa.me.agendas.list({ query: { slug: ['festival-des-lumieres'] }, }); if (error) throw error; return data.data[0] ?? null; // null: not a member, or unknown slug - lang: TypeScript label: Which agendas can I write to? source: | const { data, error } = await oa.me.agendas.list({ query: { role: ['administrator', 'moderator', 'contributor'], fields: ['title', 'role'], limit: 100, }, }); if (error) throw error; return data.data; description: > Returns a cursor-paginated list of the agendas the authenticated user is a member of, with their role on each. Private agendas the user belongs to ARE included (each item carries a `private` flag), and so are agendas not indexed in public search. security: - secretKey: [] - oauth2: - me:read parameters: - $ref: '#/components/parameters/AfterCursor' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Detailed' - $ref: '#/components/parameters/Fields' - $ref: '#/components/parameters/MeAgendaFilterSlug' - $ref: '#/components/parameters/MeAgendaFilterRole' responses: '200': description: A page of the caller's agenda memberships. content: application/json: schema: $ref: '#/components/schemas/MeAgendaList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /agendas/{agendaUid}/uploads: post: operationId: agendas.uploads.create tags: [Uploads] summary: Stage a media upload x-synonyms: [upload, stage, attach, image, file, media] x-codeSamples: - lang: TypeScript label: Stage an image, then attach it to a new event source: | // 1. Upload the file to staging (multipart/form-data). const { data: staged, error } = await oa.agendas.uploads.create({ path: { agendaUid: 12345 }, body: { file }, // a Blob/File }); if (error) throw error; // 2. Attach the ref on a normal event write. const created = await oa.agendas.events.create({ path: { agendaUid: 12345 }, body: { title: { fr: 'Concert' }, description: { fr: 'Une soirée jazz.' }, timings: [{ begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }], location: { uid: 55512 }, image: { ref: staged.ref }, }, }); if (created.error) throw created.error; return created.data; description: > Stages a media file - the native event `image`, or a custom `file`/`image` form-schema field. Send the file as `multipart/form-data` (field `file`); the API validates its real type and size, stores it, and returns a short-lived `ref`. Then attach `ref` on an event write - `image: { ref }` or a custom field under `additionalFields` as `{ ref, name? }` (`name` being the download filename) - so the media lands in the same write as the rest of the edit. The type is detected from the file content, or from the filename extension for signature-less formats such as CSV; a type outside the platform's allowed set (images, PDF and common office documents) answers `422` with the code `file.invalidType`. The file is capped at 20 MiB (`422` with the code `file.tooBig` past it). This is a coarse gate: the precise types a given field accepts are enforced on the event write, against the agenda's form schema. A request without a file answers `422` with the code `required` on `file`; a file sent in another part, or a second file, answers `422` with the code `file.unexpected` on that part. A multipart body that cannot be read (cut short, without a boundary, a text part over 1 MiB) answers `400`. security: - secretKey: [] - oauth2: - media:write parameters: - $ref: '#/components/parameters/AgendaUid' requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary description: > The media file to stage - an image, a PDF or another platform-supported document. responses: '200': description: The staging reference to attach on an event write. content: application/json: schema: $ref: '#/components/schemas/UploadTicket' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' /agendas/{agendaUid}/uploads/ticket: post: operationId: agendas.uploads.createTicket tags: [Uploads] summary: Authorize an out-of-band image upload x-synonyms: [ upload, local file, attach image, photo, picture, ticket, out of band, no base64, ] x-codeSamples: - lang: TypeScript label: Attach a LOCAL image without base64 source: | // 1. Authorize an out-of-band upload for the agenda. const { data: t, error } = await oa.agendas.uploads.createTicket({ path: { agendaUid: 12345 }, }); if (error) throw error; // 2. Upload the LOCAL file OUT OF BAND — the bytes never pass through // the model. Run this in your shell (e.g. Claude Code) or hand it // to the user; it returns { ref: "staging/…" }: // curl -sS -X POST "$UPLOAD_URL" \ // -H "X-Upload-Ticket: $TICKET" -F "file=@/path/to/image.jpg" // (uploadUrl = t.uploadUrl, ticket = t.ticket) // 3. Attach the returned ref on a normal event write. const created = await oa.agendas.events.create({ path: { agendaUid: 12345 }, body: { title: { fr: 'Concert' }, description: { fr: 'Une soirée jazz.' }, timings: [ { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00' }, ], location: { uid: 55512 }, image: { ref: '' }, }, }); if (created.error) throw created.error; return created.data; description: > Authorizes an OUT-OF-BAND upload of a LOCAL image file, made from outside this API client (e.g. with curl), so its bytes never pass through the caller's prompt. Returns a self-contained descriptor: an `uploadUrl`, a short-lived single-use `ticket`, and the `header`/`field` to use. Upload the file with a plain HTTPS `POST` to `uploadUrl`: send the `ticket` in the `X-Upload-Ticket` header and the file as multipart field `file`. That call returns a staging `ref`, which you then attach with `image: { ref }` on an event write. An image already reachable at a public `http(s)` URL is attached directly with `image: { url }` on the write, with no ticket. The ticket expires after 15 minutes (`expiresAt`) and is consumed by a successful upload. security: - secretKey: [] - oauth2: - media:write parameters: - $ref: '#/components/parameters/AgendaUid' responses: '200': description: An out-of-band upload descriptor. content: application/json: schema: $ref: '#/components/schemas/UploadDescriptor' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '503': $ref: '#/components/responses/ServiceUnavailable' /uploads/staged: post: operationId: uploads.staged tags: [Uploads] summary: Upload a file with an upload ticket x-synonyms: [upload, out of band, ticket, staged, local file, raw bytes, no base64] x-codeSamples: - lang: Shell label: Upload the raw bytes out of band (curl) source: | # uploadUrl, ticket, header and field all come from # `agendas.uploads.createTicket`. The bytes go to the upload # endpoint, outside the API client and the caller's prompt. curl -sS -X POST "$UPLOAD_URL" \ -H "X-Upload-Ticket: $TICKET" \ -F "file=@/path/to/image.jpg" # -> { "ref": "staging/12345/…", "expiresAt": "…" } description: > Receives the bytes of an OUT-OF-BAND upload authorized by `agendas.uploads.createTicket`, and returns the staging `ref` to attach on an event write (`image: { ref }` or a custom `additionalFields` field). This is the endpoint the ticket descriptor's `uploadUrl` points at. Call it with a plain HTTPS `POST` from outside the typed API client, so the file never transits the client (or an LLM's token stream). Authorize with the ticket in the `X-Upload-Ticket` header (no API key or access token) and send the file as `multipart/form-data` field `file`. The target agenda is taken from the signed ticket. The type is detected from the file content, or from the filename extension for signature-less formats such as CSV; a type outside the platform's allowed set (images, PDF and common office documents) answers `422` with the code `file.invalidType`. The file is capped at 20 MiB (`422` with the code `file.tooBig` past it). A request without a file answers `422` with the code `required` on `file`; a file sent in another part, or a second file, answers `422` with the code `file.unexpected` on that part. A multipart body that cannot be read (cut short, without a boundary, a text part over 1 MiB) answers `400`. A successful upload consumes the ticket. A consumed or expired ticket answers `401`, and uploading again takes a new ticket. A request rejected before staging (`400`, `422`) leaves the ticket usable until it expires. security: - uploadTicketAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary description: > The media file to stage - an image, a PDF or another platform-supported document. responses: '200': description: The staging reference to attach on an event write. content: application/json: schema: $ref: '#/components/schemas/UploadTicket' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/UnprocessableEntity' '503': $ref: '#/components/responses/ServiceUnavailable' /agendas/{agendaUid}/locations: get: operationId: agendas.locations.list tags: [Locations] summary: List an agenda's locations x-synonyms: [ venues, places, addresses, map, where, browse, all, find, search, name, uid, ] x-codeSamples: - lang: TypeScript label: Find a location by name source: | const { data, error } = await oa.agendas.locations.list({ path: { agendaUid: 12345 }, query: { search: 'Gare Meuse TGV', limit: 20 }, }); if (error) throw error; return data.data.map((l) => ({ uid: l.uid, name: l.name, address: l.address, })); description: > Returns a cursor-paginated list of the agenda's locations, newest first (insertion order, which can differ from `createdAt`). Pass the `after` cursor returned in `pagination.after` to fetch the next page, with the same filters. When the agenda shares its locations through a location set, the whole set is listed - including locations contributed by the other agendas of the set (their `setUid` carries the set's uid). Unknown or malformed filter values answer `400`, with per-field context under `error.errors`. An unrecognized top-level query parameter is ignored. security: - publicKey: [] - secretKey: [] - oauth2: - locations:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/AfterCursor' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Detailed' - $ref: '#/components/parameters/Fields' - $ref: '#/components/parameters/LocationSearch' - $ref: '#/components/parameters/LocationFilterUid' - $ref: '#/components/parameters/LocationFilterExtId' - $ref: '#/components/parameters/LocationBoundingBox' - $ref: '#/components/parameters/FilterCreatedAt' - $ref: '#/components/parameters/FilterUpdatedAt' responses: '200': description: A page of locations. content: application/json: schema: $ref: '#/components/schemas/LocationList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /agendas/{agendaUid}/locations/ext/{extKey}/{extId}: get: operationId: agendas.locations.getByExtId tags: [Locations] summary: Get a single location by external id x-synonyms: [ ext, external id, by ext, sync, source id, lookup, resolve, venue, place, ] x-codeSamples: - lang: TypeScript label: Resolve a location by the id it carries in a sync source source: | const { data, error } = await oa.agendas.locations.getByExtId({ path: { agendaUid: 12345, extKey: 'my-system', extId: 'abc-123' }, }); if (error) throw error; return data; description: > Returns a single location by its external identifier within the given agenda - the `(key, value)` pair an `ExtId` mapping carries in the location's `extIds`. Use this when you sync from your own system and hold its id rather than the OpenAgenda uid. Resolves to the same `Location` as the by-uid get. security: - publicKey: [] - secretKey: [] - oauth2: - locations:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/ExtKey' - $ref: '#/components/parameters/ExtIdValue' responses: '200': description: The location. content: application/json: schema: $ref: '#/components/schemas/Location' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: > Agenda or location not found. A location merged into another one answers here too, and identifies the surviving one so a stale reference can be repaired. content: application/json: schema: oneOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/MergedLocationError' examples: merged: summary: Location merged into another one value: error: code: merged message: location merged into another location mergedIn: 11111111 notFound: summary: Unknown location value: error: code: not_found message: location not found /agendas/{agendaUid}/locations/{locationUid}: get: operationId: agendas.locations.get tags: [Locations] summary: Get a location x-synonyms: [venue, place, address, detail] x-codeSamples: - lang: TypeScript label: Fetch one location source: | const { data, error } = await oa.agendas.locations.get({ path: { agendaUid: 12345, locationUid: 67890 }, }); if (error) throw error; return data; description: > Returns a single location of the agenda (or of its shared location set) in the full `Location` shape. security: - publicKey: [] - secretKey: [] - oauth2: - locations:read parameters: - $ref: '#/components/parameters/AgendaUid' - $ref: '#/components/parameters/LocationUid' responses: '200': description: The location. content: application/json: schema: $ref: '#/components/schemas/Location' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: > Agenda or location not found. A location merged into another one answers here too, and identifies the surviving one so a stale reference can be repaired. content: application/json: schema: oneOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/MergedLocationError' examples: merged: summary: Location merged into another one value: error: code: merged message: location merged into another location mergedIn: 11111111 notFound: summary: Unknown location value: error: code: not_found message: location not found /keys/verify: post: operationId: keys.verify tags: [Keys] summary: Check an API key x-synonyms: [ verify key, validate key, test key, key expired, key disabled, key scopes, introspect, ] x-codeSamples: - lang: TypeScript label: Check a key before using it source: | const { data, error } = await oa.keys.verify({ body: { key: 'oa_sk_…' }, }); if (error) throw error; return data.valid ? data.key : data.error.code; description: > Checks the API key sent in the request body. A key that does not authenticate answers `200` with `valid: false` and an `error.code` naming the reason. A check leaves the key's last use, as its owner sees it, unchanged. security: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: [key] properties: key: type: string description: > The API key to check, as it was issued. An OAuth access token answers `invalid_key`. responses: '200': description: Whether the key authenticates, and what it is. content: application/json: schema: $ref: '#/components/schemas/KeyVerification' '400': $ref: '#/components/responses/BadRequest' components: securitySchemes: oauth2: type: oauth2 description: > OAuth2 authorization code flow with PKCE (OAuth 2.1). Interactive clients redirect the user to the OpenAgenda authorization server to sign in and consent; the issued short-lived access token carries the granted scopes. PKCE (`S256`) is mandatory and public clients hold no secret. For machine-to-machine access, use an API key as a Bearer token instead. flows: authorizationCode: authorizationUrl: https://openagenda.com/api/auth/oauth2/authorize tokenUrl: https://openagenda.com/api/auth/oauth2/token refreshUrl: https://openagenda.com/api/auth/oauth2/token scopes: me:read: Read your profile, agendas and roles agendas:read: Read agendas agendas:write: Create, configure and delete agendas events:read: Read events events:write: Create, edit and delete events drafts:read: Read your event drafts events:transverse: Search events across all agendas locations:read: Read locations locations:write: Create, edit and delete locations geocoding:use: Turn addresses into coordinates, and back members:read: Read members members:write: Invite, manage and contact members media:write: Upload files bookings:read: Read bookings conversations:write: Write in OpenAgenda messaging secretKey: type: http scheme: bearer description: > Secret API key (`oa_sk_…`) sent as a Bearer token. Server-side only: it carries the user identity the writes and `/me` need, and reads as well. publicKey: type: http scheme: bearer description: > Public API key (`oa_pk_…`) sent as a Bearer token. Read-only, and it carries no user identity: it reads what is public, an operation that writes answers `403` (`read_only_credential`) and one that needs an identity answers `401` - or `403` (`insufficient_scope`) when the key carries its own scopes and they exclude the operation. A per-agenda key reads the same way. uploadTicketAuth: type: apiKey in: header name: X-Upload-Ticket description: > A single-use, short-lived upload ticket, minted by `agendas.uploads.createTicket` and sent in the `X-Upload-Ticket` header. It authorizes staging exactly ONE file for the ONE agenda it is bound to, carries no account credential, and grants nothing beyond that single upload. Used only by `POST /uploads/staged`. parameters: AgendaUid: name: agendaUid in: path required: true description: Numeric uid of the agenda. schema: type: integer format: int64 EventUid: name: eventUid in: path required: true description: Numeric uid of the event. schema: type: integer format: int64 ExtKey: name: extKey in: path required: true description: > Name of the external system/source the identifier belongs to (the `key` of an `ExtId` mapping carried on the resource — an event or a location). schema: type: string ExtIdValue: name: extId in: path required: true description: > The resource's identifier within that external system (the `value` of an `ExtId` mapping carried in the resource's `extIds`). Percent-encode reserved characters. schema: type: string MergeExtIds: name: mergeExtIds in: query required: false description: > On the by-uid `PUT`, whether the body's `extIds` are merged by key with the ones already stored (`true`, the default) or replace them wholesale (`false`). A `PATCH` merges a non-empty `extIds` whatever this parameter says; an empty array clears the stored mappings. schema: type: boolean default: true AfterCursor: name: after in: query required: false description: > Opaque pagination cursor returned in `pagination.after` of the previous page. Omit it to fetch the first page. schema: type: string Limit: name: limit in: query required: false description: > Maximum number of items to return per page. An out-of-range or non-integer value is rejected with `400`, never clamped; use the `after` cursor to page further. schema: type: integer default: 20 minimum: 1 maximum: 100 Detailed: name: detailed in: query required: false description: > When `true`, each item in `data` is returned in its detailed representation, including the detailed-only fields. When `false` (the default), items are the lighter summary representation. schema: type: boolean default: false Fields: name: fields in: query required: false description: > Comma-separated list of fields to keep on each `data` item, to shrink the payload of large pages. When set, it selects the response shape directly over the resource's full field set, so `detailed` does not apply (it only governs the default shape when `fields` is omitted) and `fields` wins if both are given. `uid` is always returned. Each resource takes the names its own `data` item declares, and a dotted path descends one segment per level: on an event, `location.name` or `timings.begin`. An unknown top-level field is rejected with `400`, and so is an unknown sub-field under a closed object (`location.zzz`); under an open container - an `additionalFields` custom-field bag, on the resources that carry one, or a localized text map - any sub-key is accepted and yields nothing when absent (`additionalFields.myField`). Response schemas stay complete, so a generated client still types the omitted fields as present — read them as optional on this path. schema: type: array items: type: string example: [uid, slug] style: form explode: false AgendaSearch: name: search in: query required: false description: > Full-text search across agenda title and description. Wrap the value in double quotes for an exact phrase match. schema: type: string AgendaSort: name: sort in: query required: false description: > Sort order. When omitted, results are ranked by relevance if `search` is set, otherwise by `createdAt` descending. Set `recentlyAddedEvents.desc` to surface the agendas with the most recent activity first. schema: type: string enum: - createdAt.desc - recentlyAddedEvents.desc AgendaFilterUid: name: uid in: query required: false description: Restrict to these agenda uids. Repeat the parameter for multiple values. schema: type: array items: type: integer format: int64 AgendaFilterSlug: name: slug in: query required: false description: Restrict to these agenda slugs. Repeat the parameter for multiple values. schema: type: array items: type: string MeAgendaFilterSlug: name: slug in: query required: false description: > Restrict to the agendas carrying these slugs. A slug matching none of your memberships yields an empty page. Repeat the parameter for multiple values. schema: type: array items: type: string MeAgendaFilterRole: name: role in: query required: false description: > Restrict to the memberships holding one of these roles. Repeat the parameter for multiple values. schema: type: array items: $ref: '#/components/schemas/MemberRole' AgendaFilterOfficial: name: official in: query required: false description: > Restrict to official agendas (`true`) or to non-official agendas (`false`). Omit to return both. schema: type: boolean LocationUid: name: locationUid in: path required: true description: Numeric uid of the location. schema: type: integer format: int64 LocationSearch: name: search in: query required: false description: > Free-text search over the locations' name, address, city, region and department. schema: type: string LocationFilterUid: name: uid in: query required: false description: > Restrict to the given location uid(s). Repeat the parameter (`uid=123&uid=456`) for multiple values. style: form explode: true schema: type: array items: type: integer format: int64 LocationFilterExtId: name: extId in: query required: false style: deepObject explode: true description: > Match a location by an external identifier: `extId[key]=&extId[value]=`. Both parts are required. schema: type: object additionalProperties: false required: [key, value] properties: key: type: string value: type: string LocationBoundingBox: name: bbox in: query required: false description: > Restrict to locations within a bounding box, given as `west,south,east,north` in decimal degrees (WGS84). Example: `bbox=2.224,48.815,2.469,48.902`. schema: type: string pattern: '^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$' Facets: name: facets in: query required: true description: > Comma-separated list of facets to compute over the filtered events. Unknown facet names return `400`. Each facet's result shape is defined by `FacetResults`. Bucket-list facets (the term, provenance and `locations` families) return the top buckets by event count, capped by `facetSize` (overridable per facet with `facetSizes[]`) and reordered by `facetSort`; past the cap, narrow the filter. `additionalFields` and `additionalFieldMetrics` cover the agenda's own fields as its event form schema declares them (`GET /agendas/{agendaUid}/events/schema`), named by `additionalFieldsKeys`/`additionalFieldMetricsKeys` or all readable fields when omitted; `additionalFields` returns one bucket per option. `timings` bucket width is set by `timingsInterval`, the `dateRanges` month by `month`, `geohash` granularity by `geohashZoom`. style: form explode: false schema: type: array minItems: 1 items: type: string enum: - additionalFields - additionalFieldMetrics - cities - regions - departments - districts - countryCodes - keywords - languages - accessibilities - status - attendanceModes - originAgendas - sourceAgendas - locations - geohash - viewport - timespan - timings - dateRanges FacetSize: name: facetSize in: query required: false description: > Default maximum number of buckets for the bucket-list facets (the term, provenance and `locations` families), ordered by event count. Override a single facet with `facetSizes[]`. Out-of-range values are clamped; other facets ignore it. schema: type: integer default: 10 minimum: 1 maximum: 250 FacetSizes: name: facetSizes in: query required: false description: > Per-facet override of `facetSize`, keyed by facet name (`facetSizes[cities]=50`). Precedence: per-facet override > `facetSize` > default (10). Only applies to bucket-list facets; values are clamped to [1, 250]. style: deepObject explode: true schema: type: object additionalProperties: type: integer minimum: 1 maximum: 250 FacetSort: name: facetSort in: query required: false description: > Default ordering of the bucket-list facets. `count` (default) returns the most frequent buckets first; `alpha` returns the same top-`facetSize` buckets but ordered alphabetically by their display value (city/keyword name, location name, agenda title) for readable scanning. Override a single facet with `facetSorts[]`. schema: type: string default: count enum: - count - alpha FacetSorts: name: facetSorts in: query required: false description: > Per-facet override of `facetSort`, keyed by facet name (`facetSorts[cities]=alpha`). Precedence: per-facet override > `facetSort` > default (`count`). style: deepObject explode: true schema: type: object additionalProperties: type: string enum: - count - alpha FacetMissing: name: facetMissing in: query required: false description: > Per-facet label for a synthetic bucket counting the filtered events that have **no value** for the facet's field (`facetMissing[district]=Unknown` adds an `{ value: "Unknown", count }` bucket). Honoured by the term facets (those whose buckets are keyed by the displayed value) except `accessibilities`; ignored by the provenance, `locations` and remaining families. The bucket appears only when such events exist. style: deepObject explode: true schema: type: object additionalProperties: type: string GeohashZoom: name: geohashZoom in: query required: false description: > Clustering zoom level for the `geohash` facet (higher = finer grid, more clusters). Ignored unless `geohash` is requested. schema: type: integer default: 1 minimum: 1 TimingsInterval: name: timingsInterval in: query required: false description: > Bucket width for the `timings` date histogram. Ignored unless `timings` is requested; an unrecognized value falls back to `day`. schema: type: string default: day enum: - hour - day - week - month - year Month: name: month in: query required: false description: > Calendar month (`YYYY-MM`) the `dateRanges` facet buckets day-by-day. Defaults to the current month; an unrecognized value falls back to it. Ignored unless `dateRanges` is requested. Because it bounds the buckets by scoping the filtered set to that month, it also constrains any other facet requested in the same call. schema: type: string pattern: '^\d{4}-\d{2}$' AdditionalFieldsKeys: name: additionalFieldsKeys in: query required: false description: > Comma-separated agenda choice/boolean field names to compute the `additionalFields` facet on — the names the agenda's event form schema declares (`GET /agendas/{agendaUid}/events/schema`). Omit to get every such field readable at the caller's access level. An unknown or non-readable field name returns `400`. Ignored unless `additionalFields` is requested. style: form explode: false schema: type: array items: type: string AdditionalFieldMetricsKeys: name: additionalFieldMetricsKeys in: query required: false description: > Comma-separated agenda numeric field names to compute the `additionalFieldMetrics` facet on — the names the agenda's event form schema declares (`GET /agendas/{agendaUid}/events/schema`). Omit to get every numeric field readable at the caller's access level. An unknown or non-readable field name returns `400`. Ignored unless `additionalFieldMetrics` is requested. style: form explode: false schema: type: array items: type: string Sort: name: sort in: query required: false description: > Ordering of the results. Defaults to `timingsWithFeatured.asc`, or to `score` (relevance) when `search` is set and no `sort` is given. schema: type: string enum: - timings.asc - timingsWithFeatured.asc - lastTiming.asc - lastTimingWithFeatured.asc - updatedAt.asc - updatedAt.desc - availabilityUpdatedAt.asc - availabilityUpdatedAt.desc - location.name.asc - location.name.desc - location.city.asc - location.city.desc - score x-enum-descriptions: timings.asc: By event date, earliest first timingsWithFeatured.asc: By event date, featured events first lastTiming.asc: By last occurrence date, earliest first lastTimingWithFeatured.asc: By last occurrence, featured events first updatedAt.asc: Least recently updated first updatedAt.desc: Most recently updated first availabilityUpdatedAt.asc: Oldest inventory write first; events with no inventory last availabilityUpdatedAt.desc: Newest inventory write first; events with no inventory last location.name.asc: By location name, A→Z location.name.desc: By location name, Z→A location.city.asc: By city, A→Z location.city.desc: By city, Z→A score: By search relevance (only meaningful with `search`) Search: name: search in: query required: false description: > Full-text search across title, description, keywords and address. Wrap the value in double quotes for an exact phrase match. schema: type: string RelevanceThreshold: name: threshold in: query required: false description: > Relevance score floor for `search` results, used to drop weakly-matching events from the long tail (e.g. a single query term buried in a long description). Ignored when `search` is not set. `off` (the default) returns every match. `auto` derives the floor from the score distribution of the current query: the largest relative drop ("elbow") in the top scores becomes the cutoff, so it adapts to each query. A non-negative number is an absolute minimum score; relevance scores are specific to a query, so a fixed value suits a single tuned query rather than general use - prefer `auto`. The cutoff applies before counting, so the returned events, their `total` and the facet counts describe the same trimmed set; pass the same `threshold` to the facets endpoint to keep its counts aligned with the list. schema: # Quote the string barewords: unquoted `off` is coerced to boolean false # by YAML 1.1 tooling (PyYAML, some codegens), diverging from the runtime # string. (The server also accepts a falsy `false`/"false" as `off`.) default: 'off' oneOf: - type: string enum: ['off', 'auto'] - type: number minimum: 0 FilterUid: name: uid in: query required: false description: Restrict to these event uids. Repeat the parameter for multiple values. schema: type: array items: type: integer format: int64 FilterSlug: name: slug in: query required: false description: Restrict to these event slugs. Repeat the parameter for multiple values. schema: type: array items: type: string FilterExtId: name: extId in: query required: false style: deepObject explode: true description: > Match an event by an external identifier: `extId[key]=&extId[value]=`. schema: type: object additionalProperties: false required: [key, value] properties: key: type: string value: type: string FilterKeyword: name: keyword in: query required: false description: > Restrict to events tagged with these keywords. Repeating the parameter narrows the result set (AND semantics). schema: type: array items: type: string FilterLanguage: name: language in: query required: false description: Restrict to events in these languages (ISO 639-1 codes). schema: type: array items: type: string FilterAccessibility: name: accessibility in: query required: false description: Restrict to events offering these accessibility facilities. schema: type: array items: $ref: '#/components/schemas/AccessibilityCode' FilterStatus: name: status in: query required: false description: Restrict to these statuses. schema: type: array items: $ref: '#/components/schemas/EventStatus' FilterAttendanceMode: name: attendanceMode in: query required: false description: Restrict to these attendance modes. schema: type: array items: $ref: '#/components/schemas/AttendanceMode' FilterFeatured: name: featured in: query required: false description: Restrict to featured (`true`) or non-featured (`false`) events. schema: type: boolean FilterLocationUid: name: locationUid in: query required: false description: Restrict to events held at these location uids. schema: type: array items: type: integer format: int64 FilterLocationExtId: name: locationExtId in: query required: false style: deepObject explode: true description: > Match by a location external identifier: `locationExtId[key]=&locationExtId[value]=`. schema: type: object additionalProperties: false required: [key, value] properties: key: type: string value: type: string FilterRegion: name: region in: query required: false description: Restrict to events located in these regions. schema: type: array items: type: string FilterDepartment: name: department in: query required: false description: Restrict to events located in these departments. schema: type: array items: type: string FilterCity: name: city in: query required: false description: Restrict to events located in these cities. schema: type: array items: type: string FilterDistrict: name: district in: query required: false description: Restrict to events located in these districts. schema: type: array items: type: string FilterAdminLevel3: name: adminLevel3 in: query required: false description: Restrict to events located in these level-3 administrative areas. schema: type: array items: type: string FilterAdminLevel5: name: adminLevel5 in: query required: false description: Restrict to events located in these level-5 administrative areas. schema: type: array items: type: string FilterCountryCode: name: countryCode in: query required: false description: Restrict to events located in these countries (ISO 3166-1 alpha-2). schema: type: array items: type: string pattern: '^[A-Za-z]{2}$' FilterBoundingBox: name: bbox in: query required: false description: > Restrict to events whose location falls within a bounding box, given as `west,south,east,north` in decimal degrees (WGS84). Example: `bbox=2.224,48.815,2.469,48.902`. schema: type: string pattern: '^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$' FilterNear: name: near in: query required: false description: > Center of a proximity search, given as `latitude,longitude` in decimal degrees. Must be combined with `radius`. schema: type: string pattern: '^-?\d+(\.\d+)?,-?\d+(\.\d+)?$' FilterRadius: name: radius in: query required: false description: > Proximity radius in metres around `near`. Requires `near` to be set. schema: type: integer minimum: 1 FilterOriginAgendaUid: name: originAgendaUid in: query required: false description: Restrict to events originally created in these agendas. schema: type: array items: type: integer format: int64 FilterOriginAgendaOfficial: name: originAgendaOfficial in: query required: false description: Restrict to events whose origin agenda is official (`true`) or not (`false`). schema: type: boolean FilterSourceAgendaUid: name: sourceAgendaUid in: query required: false description: Restrict to events aggregated from these source agendas. schema: type: array items: type: integer format: int64 FilterRelative: name: relative in: query required: false description: > Restrict by temporal position relative to now. Combine values (repeat the parameter) to widen the window: `passed`, `upcoming`, `current`. schema: type: array items: type: string enum: [passed, upcoming, current] FilterTimings: name: timings in: query required: false style: deepObject explode: true description: > Restrict to events having a timing that begins within the range, as RFC 3339 date-times: `timings[gte]=…&timings[lte]=…`. For now-relative windows, prefer `relative`. Date-times must carry an explicit offset (e.g. `Z`); a bare date (`YYYY-MM-DD`) is read as UTC midnight. schema: type: object additionalProperties: false properties: gte: type: string format: date-time lte: type: string format: date-time FilterLocalTime: name: localTime in: query required: false style: deepObject explode: true description: > Restrict to events with a timing starting within a daily time window, in minutes from local midnight: `localTime[gte]=480` (08:00) and/or `localTime[lte]=1200` (20:00). schema: type: object additionalProperties: false properties: gte: type: integer minimum: 0 maximum: 1440 lte: type: integer minimum: 0 maximum: 1440 FilterCreatedAt: name: createdAt in: query required: false style: deepObject explode: true description: > Restrict by creation date, as RFC 3339 date-times: `createdAt[gte]=…&createdAt[lte]=…`. Date-times must carry an explicit offset (e.g. `Z`); a bare date (`YYYY-MM-DD`) is read as UTC midnight. schema: type: object additionalProperties: false properties: gte: type: string format: date-time lte: type: string format: date-time FilterUpdatedAt: name: updatedAt in: query required: false style: deepObject explode: true description: > Restrict by last-update date, as RFC 3339 date-times: `updatedAt[gte]=…&updatedAt[lte]=…`. Useful for incremental sync. Date-times must carry an explicit offset (e.g. `Z`); a bare date (`YYYY-MM-DD`) is read as UTC midnight. schema: type: object additionalProperties: false properties: gte: type: string format: date-time lte: type: string format: date-time FilterAvailabilityUpdatedAt: name: availabilityUpdatedAt in: query required: false style: deepObject explode: true description: > Restrict by the date OpenAgenda last WROTE inventory on the event — the write clock carried by `availabilityUpdatedAt`, not the rows' own `syncedAt`. As RFC 3339 date-times: `availabilityUpdatedAt[gte]=…&availabilityUpdatedAt[lte]=…`. The inventory counterpart of `updatedAt`, for a consumer following availability: pair it with `sort=availabilityUpdatedAt.asc` and a watermark. Events that never received availability carry no stamp and never match — following inventory does not mean paging through the whole corpus. Same date-time rules as `updatedAt`. schema: type: object additionalProperties: false properties: gte: type: string format: date-time lte: type: string format: date-time FilterAge: name: age in: query required: false style: deepObject explode: true description: > Restrict to events whose intended audience age range overlaps the given bounds, in years: `age[gte]=7&age[lte]=12`. schema: type: object additionalProperties: false properties: gte: type: integer minimum: 0 lte: type: integer minimum: 0 FilterAdditionalFields: name: additionalFields in: query required: false style: deepObject explode: true description: > Filter on agenda-specific additional fields, addressed by field name: `additionalFields[]=`. Available fields and their value types come from the agenda's event form schema (`GET /agendas/{agendaUid}/events/schema`). For numeric fields, use a range object: `additionalFields[][gte]=…&additionalFields[][lte]=…`. schema: type: object additionalProperties: x-additionalPropertiesName: fieldName responses: BadRequest: description: > Malformed request: the body is not valid JSON, it carries a field the operation does not accept, or a recognized parameter carries an invalid value. `error.errors`, when present, names the offending values. content: application/json: schema: oneOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/ValidationError' Unauthorized: description: >- The request carries no credential this operation accepts. content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: >- Authenticated, but not allowed to access this resource. A blacklisted account is refused like a member the agenda does not allow to perform the operation. On an `insufficient_scope` refusal a `WWW-Authenticate: Bearer error="insufficient_scope", scope=""` header names the missing scope (RFC 6750 §3.1); an API key carries scopes only when it declares its own permissions. headers: WWW-Authenticate: description: >- Present on an `insufficient_scope` failure: the RFC 6750 Bearer challenge naming the scope the token must hold. required: false schema: type: string example: >- Bearer error="insufficient_scope", scope="events:read", error_description="the access token lacks the required scope" content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: No resource answers this identifier. content: application/json: schema: $ref: '#/components/schemas/Error' UnprocessableEntity: description: >- The request was well-formed but its field values failed validation (e.g. a missing title, an invalid timing, an unknown location uid). content: application/json: schema: $ref: '#/components/schemas/ValidationError' ServiceUnavailable: description: >- The request was not processed and can be retried after a short delay. content: application/json: schema: $ref: '#/components/schemas/Error' schemas: Error: type: object description: A refused request, carrying nothing but the reason. required: [error] additionalProperties: false properties: error: type: object required: [code, message] additionalProperties: false properties: code: type: string description: Stable, machine-readable error code. enum: [ bad_request, unauthorized, read_only_credential, insufficient_scope, forbidden, not_found, service_unavailable, ] x-enum-descriptions: read_only_credential: >- 403 - the credential may only read, and this operation writes insufficient_scope: >- 403 - the credential lacks the scope this operation declares forbidden: 403 - any other refusal of an authenticated caller message: type: string description: Human-readable explanation. KeyVerification: description: The outcome of checking an API key. oneOf: - $ref: '#/components/schemas/ValidKey' - $ref: '#/components/schemas/InvalidKey' ValidKey: type: object description: A key that authenticates. required: [valid, key] additionalProperties: false properties: valid: type: boolean enum: [true] key: $ref: '#/components/schemas/VerifiedKey' InvalidKey: type: object description: A key that does not authenticate, and why. required: [valid, error] additionalProperties: false properties: valid: type: boolean enum: [false] error: type: object required: [code, message] additionalProperties: false properties: code: type: string description: Stable, machine-readable reason. enum: [ invalid_key, key_disabled, key_expired, key_exhausted, key_unreadable, owner_blacklisted, ] x-enum-descriptions: invalid_key: >- the key is unknown or revoked, or the account that owns it is gone key_disabled: its owner switched the key off and can switch it back on key_expired: the key has passed its expiry date key_exhausted: the key has used up its request allowance key_unreadable: >- the key is stored in a state that cannot be read; contact support owner_blacklisted: the account that owns the key is blacklisted message: type: string description: Human-readable explanation. VerifiedKey: type: object description: What a working key is and what it may do. required: [type, owner, scopes, expiresAt] additionalProperties: false properties: type: type: string enum: [public, secret, agenda] description: > `public` reads what is public and carries no user identity, `secret` acts as the user who owns it, `agenda` belongs to an agenda rather than to a user. owner: $ref: '#/components/schemas/KeyOwner' scopes: type: [array, 'null'] items: type: string description: > The scopes the key is restricted to. `null` when the key declares no scopes of its own, so that its `type` alone bounds what it can do. expiresAt: type: [string, 'null'] format: date-time description: > The instant the key stops authenticating, or `null` when it does not expire. KeyOwner: type: [object, 'null'] description: > Who the key belongs to; `null` for a public key. required: [type, uid] additionalProperties: false properties: type: type: string enum: [user, agenda] uid: type: integer format: int64 description: The uid of that user or agenda. ValidationError: type: object description: A request refused because of the values it carries. required: [error] additionalProperties: false properties: error: type: object required: [code, message, errors] additionalProperties: false properties: code: type: string description: Stable, machine-readable error code. enum: [validation_error, bad_request] message: type: string description: Human-readable explanation. errors: type: array description: The problems found in the request, one per value. minItems: 1 items: $ref: '#/components/schemas/ValidationIssue' ValidationIssue: type: object description: >- One problem found in a request. A validator describes the values it refuses as precisely as it can, so an item carries the keys below plus whatever else that validator knows, such as the bounds the value had to satisfy. required: [message] properties: field: type: string description: >- Where the offending value is in the request, as a dotted path. It is absent when the problem belongs to no single value. examples: [ 'title', 'timings', 'timings.end', 'additionalFields.thematique', 'image.url', ] lang: type: string description: Language key of the offending value, on a localized field. index: type: integer description: Position of the offending value, in a list field. code: type: string description: Stable, machine-readable reason for the refusal. examples: ['required', 'string.toolong', 'email.invalid'] message: type: string description: Human-readable explanation, in English. input: description: >- The value the validator refused, as the request carried it. A validator that reports a problem without recording the value omits this key. ValidationVerdict: type: object description: >- The verdict of a dry run, reached when validation found nothing to refuse. required: [valid] additionalProperties: false properties: valid: type: boolean enum: [true] MergedLocationError: type: object description: >- An error response for a location that was merged into another one. required: [error] additionalProperties: false properties: error: type: object required: [code, message, mergedIn] additionalProperties: false properties: code: type: string description: Stable, machine-readable error code. enum: [merged] message: type: string description: Human-readable explanation. mergedIn: type: integer description: Uid of the location this one was merged into. Pagination: type: object description: >- Where a page of results stops, and how to ask for the next one. required: [after, limit] additionalProperties: false example: after: eyJ1aWQiOjIzNDU2fQ limit: 20 total: 342 totalRelation: exact properties: after: type: [string, 'null'] description: > Opaque cursor to fetch the next page (pass back as the `after` query parameter). `null` when there are no more results; a full last page can still carry a cursor whose next page is empty. limit: type: integer description: The page size that was applied. total: type: integer description: > Total number of matching records, when the operation counts them. totalRelation: type: string enum: [exact, atLeast] description: > Whether `total` is the exact count (`exact`) or a lower bound (`atLeast`). Very large result sets are counted only up to a limit; past it `total` is the floor, not the precise number. Present whenever `total` is. LocalizedString: type: object description: > A string localized per language, keyed by ISO 639-1 code (e.g. `fr`, `en`). additionalProperties: type: string x-additionalPropertiesName: languageCode examples: - fr: 'Concert de jazz' en: 'Jazz concert' LocalizedStringArray: type: object description: A list of strings localized per language code (used for keywords). additionalProperties: type: array x-additionalPropertiesName: languageCode items: type: string Image: type: object description: > Image of an event, an agenda or a location, rendered on demand from a single source. Use `src` as a ready-to-use default rendition, or drive a responsive picture: substitute a geometry into `srcTemplate` or pick a ready-made width from `srcset`. `width`/`height` are the source dimensions, which cap every rendition. additionalProperties: false required: [width, height, src, srcTemplate, srcset] properties: width: type: [integer, 'null'] description: Intrinsic width of the source image, in pixels. height: type: [integer, 'null'] description: Intrinsic height of the source image, in pixels. src: type: [string, 'null'] format: uri description: > Ready-to-use default rendition URL (a naive `` just works). srcTemplate: type: [string, 'null'] description: > URL with a `{geo}` placeholder to substitute with a geometry, to render the image at any size. Supported geometries: `{W}x{H}`, where `0` on an axis means proportional to the other (e.g. `800x0`); append `/smart` to crop on the detected point of interest rather than the centre, or prefix `fit-in/` to contain the image instead of cropping it (e.g. `300x300`, `300x300/smart`, `fit-in/1000x1000`). `null` when no rendition can be derived - use `src` as is. srcset: type: array description: Ready-made proportional widths, never upscaled past the source. items: type: object additionalProperties: false properties: width: type: integer url: type: string format: uri EventLocation: type: object description: > The location snapshot an event carries - a nullable subset of the `Location` record. The canonical, full record is the `Location` resource (`GET /agendas/{agendaUid}/locations/{locationUid}`, same `uid`). additionalProperties: false properties: uid: type: integer format: int64 readOnly: true name: type: [string, 'null'] address: type: [string, 'null'] city: type: [string, 'null'] region: type: [string, 'null'] postalCode: type: [string, 'null'] countryCode: type: [string, 'null'] description: ISO 3166-1 alpha-2 country code. latitude: type: [number, 'null'] format: double longitude: type: [number, 'null'] format: double district: type: [string, 'null'] department: type: [string, 'null'] adminLevel3: type: [string, 'null'] adminLevel5: type: [string, 'null'] country: oneOf: - $ref: '#/components/schemas/LocalizedString' - type: 'null' description: Localized country label. timezone: type: [string, 'null'] description: IANA time zone of the location. LocationSummary: type: object description: > Compact location representation returned by the list endpoint (`detailed: false`) — identity, coordinates and verification status only; fetch with `detailed=true` or the single-location get for the full record. additionalProperties: false example: uid: 67890 name: Château des Lèbres address: Naves, 07140 Les Vans latitude: 44.388303 longitude: 4.105528 verified: true required: - uid - name - address - latitude - longitude - verified properties: uid: type: integer format: int64 readOnly: true name: type: string address: type: [string, 'null'] latitude: type: number format: double longitude: type: number format: double verified: type: boolean description: Whether the location was verified by an agenda moderator. Location: type: object description: > Full location representation returned by the single-get endpoint and by the list when `detailed=true` — the canonical record (the events' embedded `EventLocation` is a nullable snapshot of it). additionalProperties: false example: uid: 67890 slug: chateau-des-lebres_4421007 setUid: null name: Château des Lèbres address: Naves, 07140 Les Vans city: Les Vans district: null region: Auvergne-Rhône-Alpes department: Ardèche adminLevel3: null adminLevel5: null postalCode: '07140' insee: '07334' countryCode: FR latitude: 44.388303 longitude: 4.105528 timezone: Europe/Paris description: fr: Ancienne maison forte des comtes du Roure. access: {} image: width: 1200 height: 800 src: https://img.openagenda.com/u/1080x0/main/67890.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/67890.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/67890.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/67890.full.image.jpg imageCredits: null website: http://www.domainedeslebres.com/ email: null phone: '0475363414' links: - https://fr-fr.facebook.com/domainedeslebres/ extIds: - key: legacy value: '4421007' additionalFields: tags: - id: 2 label: Château, hôtel urbain, palais, manoir siret: null verified: true createdAt: '2017-10-30T14:21:07.000Z' updatedAt: '2019-04-29T15:40:33.000Z' required: - uid - slug - setUid - name - address - city - district - region - department - adminLevel3 - adminLevel5 - postalCode - insee - countryCode - latitude - longitude - timezone - description - access - image - imageCredits - website - email - phone - links - extIds - additionalFields - siret - verified - createdAt - updatedAt properties: uid: type: integer format: int64 readOnly: true slug: type: string readOnly: true setUid: type: [integer, 'null'] format: int64 readOnly: true description: > Uid of the shared location set this location belongs to, or `null` when it belongs to a single agenda's own pool. name: type: string address: type: [string, 'null'] city: type: [string, 'null'] district: type: [string, 'null'] region: type: [string, 'null'] department: type: [string, 'null'] adminLevel3: type: [string, 'null'] adminLevel5: type: [string, 'null'] postalCode: type: [string, 'null'] insee: type: [string, 'null'] description: INSEE code of the French municipality, when known. countryCode: type: [string, 'null'] description: ISO 3166-1 alpha-2 country code. latitude: type: number format: double longitude: type: number format: double timezone: type: [string, 'null'] description: IANA time zone of the location. description: $ref: '#/components/schemas/LocalizedString' access: allOf: - $ref: '#/components/schemas/LocalizedString' description: > Localized practical information on how to reach the location. image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' imageCredits: type: [string, 'null'] website: type: [string, 'null'] email: type: [string, 'null'] phone: type: [string, 'null'] links: type: array items: type: string extIds: type: array items: $ref: '#/components/schemas/LocationExtId' additionalFields: $ref: '#/components/schemas/LocationAdditionalFields' siret: type: [string, 'null'] description: >- SIRET number (French establishment id) of the location's operator, when provided. verified: type: boolean description: Whether the location was verified by an agenda moderator. createdAt: type: string format: date-time readOnly: true updatedAt: type: string format: date-time readOnly: true LocationExtId: type: object description: > An external identifier mapping this location to a third-party system. additionalProperties: false properties: key: type: string value: type: [string, 'null'] LocationAdditionalFields: type: object description: > Agenda-specific additional fields of the location, keyed by field name. `tags` carries the location's tags, filtered against the tag set declared in the agenda's schema. properties: tags: type: array items: type: object additionalProperties: false properties: id: type: integer label: description: > Display label of the tag — a plain string or a localized map, as stored. oneOf: - type: string - $ref: '#/components/schemas/LocalizedString' additionalProperties: x-additionalPropertiesName: fieldName LocationList: type: object required: [data, pagination] additionalProperties: false properties: data: type: array description: > `LocationSummary` items by default, or full `Location` items when `detailed=true`. items: # Detailed branch FIRST: order is irrelevant to JSON Schema (the # branches are mutually exclusive) but the generated zod client # returns the first match and strips unknown keys — summary-first # would silently strip the detailed-only fields. Same rule on every # summary/detailed pair. oneOf: - $ref: '#/components/schemas/Location' - $ref: '#/components/schemas/LocationSummary' pagination: $ref: '#/components/schemas/Pagination' EventFormSchema: type: object description: The event form an agenda declares. required: [fields] # Served from the form engine, its descriptors filtered by access and # their `read`/`write` levels normalized to lists: top-level keys beyond # `fields` may appear (engine vocabulary); only `fields` is contractual. additionalProperties: true properties: fields: type: array description: > The fields of the merged schema readable at the caller's access level - native event fields (with any per-agenda overrides applied) and the agenda's/network's additional fields (`path` under `additionalFields`). A field whose `read` access levels exclude the caller is omitted, and so is a native field the write operations do not take. items: $ref: '#/components/schemas/FormSchemaField' FormSchemaField: type: object description: > A form field descriptor. Two kinds share the array: **data field descriptors** (carrying `field` and the `path` of its value) and **section separators** (`type: 'section'`, no `field` - structure the form into titled groups). Descriptors may carry keys beyond those listed below. additionalProperties: true example: field: thematique path: additionalFields.thematique fieldType: radio label: { fr: 'Thématique', en: 'Theme' } schemaId: 10522 schemaType: agenda options: - id: 42 value: concert label: { fr: 'Concert' } properties: field: type: string description: > Field name. Absent on section separators. path: type: string description: > Where the field's value lives on an event, as a dotted path: `additionalFields.` for an additional field, the field name itself for a native one. Absent on section separators. fieldType: type: string description: > Field kind, which sets the shape of the value (e.g. `text`, `radio`, `number`, `image`). type: type: string description: > Descriptor kind marker - `section` for section separators. slug: type: string description: Stable identifier of a section separator. label: description: Localized display label, when the descriptor carries one. oneOf: - $ref: '#/components/schemas/LocalizedString' - type: string - type: 'null' optional: type: boolean description: > `false` means the field is required (possibly a per-agenda override of a native field). display: description: Whether the field is shown on the form (default true). enable: type: boolean description: Whether the field is enabled (default true). enableWith: description: > The field is only active when another field has a value: either that field's name, or `{ field, value }` to require specific values (e.g. `onlineAccessLink` enabled when `attendanceMode` is online or mixed). While inactive, the field is not required, whatever `optional` says, and holds no value: one sent is not kept, and a stored one is cleared. optionalWith: description: > The field is optional when another field has a value (either that field's name, or `{ field, value }` to require specific values) and required otherwise; an explicit `optional` takes precedence. read: type: [array, 'null'] items: $ref: '#/components/schemas/AccessLevel' description: > Access levels allowed to read the field's value. `null` or empty means every caller. write: type: [array, 'null'] items: $ref: '#/components/schemas/AccessLevel' description: > Access levels allowed to write the field's value. `null` or empty means every caller allowed to write events on the agenda; otherwise your access level must be listed, and a value you send for the field answers `422` with an `unauthorized` issue on it. origin: type: [string, 'null'] enum: [tags, categories, custom, null] description: > Set on a field converted from an earlier agenda setting: what it was converted from. options: type: array description: Allowed choices for choice-like field kinds. items: type: object additionalProperties: true properties: id: type: integer description: >- What an additional field carries for this option. A value that is no option's `id`, the option's `value` included, answers `422` on a write. value: type: string label: oneOf: - $ref: '#/components/schemas/LocalizedString' - type: string schemaId: type: [integer, 'null'] description: > Identifier of the agenda or network schema that declared or overrode the field; `null` on a native field as the platform declares it. schemaType: type: [string, 'null'] enum: [agenda, network, event, null] description: > Which level declared or overrode the field: `agenda` or `network`, or `event` for a native field as the platform declares it. AccessLevel: type: string description: > A level of access to an agenda's data, as the field rules of its event form name them. enum: [ public, reader, contributor, moderator, administrator, internal, system, ] x-enum-descriptions: public: A caller without a membership on the agenda reader: A member with the reader role contributor: A member with the contributor role moderator: A member with the moderator role administrator: A member with the administrator role internal: The platform's own processes, never an API caller system: The platform's own processes, never an API caller MemberRole: type: string enum: [administrator, moderator, contributor, reader] x-enum-descriptions: administrator: Full control of the agenda, its settings and members moderator: Moderates contributed events and manages content contributor: Can contribute events to the agenda reader: Read-only member MeAgendaItem: type: object description: > An agenda the authenticated user is a member of: the AgendaSummary base fields plus the user's `role` and the agenda's `private` flag (private agendas the user belongs to are listed). additionalProperties: false example: uid: 12345 slug: festival-des-lumieres title: Festival des Lumières description: La programmation du festival. image: width: 1400 height: 900 src: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/12345.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/12345.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg official: false private: false role: administrator required: - uid - slug - title - description - image - official - private - role properties: uid: type: integer format: int64 readOnly: true slug: type: string readOnly: true title: type: [string, 'null'] description: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' official: type: boolean private: type: boolean role: $ref: '#/components/schemas/MemberRole' MeAgendaItemDetailed: type: object description: > The `detailed=true` variant of `MeAgendaItem`: the membership item plus `createdAt`, `network` and `locationSet`. additionalProperties: false required: - uid - slug - title - description - image - official - private - role - createdAt - network - locationSet properties: uid: type: integer format: int64 readOnly: true slug: type: string readOnly: true title: type: [string, 'null'] description: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' official: type: boolean private: type: boolean role: $ref: '#/components/schemas/MemberRole' createdAt: type: [string, 'null'] format: date-time network: $ref: '#/components/schemas/AgendaNetworkRef' locationSet: $ref: '#/components/schemas/AgendaLocationSetRef' MeAgendaList: type: object required: [data, pagination] additionalProperties: false properties: data: type: array description: > `MeAgendaItem` items by default, or `MeAgendaItemDetailed` items when `detailed=true`. items: # Detailed branch first — see LocationList for why. oneOf: - $ref: '#/components/schemas/MeAgendaItemDetailed' - $ref: '#/components/schemas/MeAgendaItem' pagination: $ref: '#/components/schemas/Pagination' Timing: type: object description: One occurrence of an event. required: [begin, end] additionalProperties: false properties: begin: type: string format: date-time end: type: string format: date-time id: type: string pattern: '^[0-9ABCDEFGHJKMNPQRSTVWXYZ]{6}$' description: > Stable identifier of this occurrence, kept across writes so a link to a single date keeps working when the event is edited. Absent (rather than `null`) when none has been assigned. The server never moves a stored `id` to another occurrence; an occurrence it cannot match to a stored one receives a fresh `id`. Supplied on write, it is stored as sent and names that occurrence; a value repeated within the submitted timings is kept on the first occurrence only. A supplied `id` that does not match the pattern answers `422`. sourceRef: type: string maxLength: 255 description: > Your own key for this occurrence, opaque to OpenAgenda and stored as sent. On a resync, an occurrence carrying the same `sourceRef` keeps its `id` even when its dates moved, which is how a connector holds on to an occurrence it has already published. Two occurrences of the same event sharing a `sourceRef` are both treated as new. availability: type: array maxItems: 7 description: > The inventory of this occurrence, ONE ROW PER TICKETING PROVIDER — the same occurrence can be sold out on pass Culture and open to the general public. At most one row per provider, so never more rows than the `provider` enum has values. Absent on an occurrence that never received any; `[]` is a value the field can hold, since an empty array sent with an occurrence the event does not have yet is stored and served as such. WRITTEN BY THE AVAILABILITY ENDPOINT. An ordinary event write can only SEED it, on an occurrence the event does not have yet — see below — and never change, remove or clear a row of one it already has. That endpoint is NOT part of this contract: it is `PATCH /v2/agendas/{agendaUid}/events/{eventUid}/availability`, on the v2 API, and this document declares no path for it, so a client generated from this spec cannot reach it and has to call it directly. Its body is keyed by OCCURRENCE first and by provider second — `{ "": { "": { … } } }` — and `null` in place of a row removes that provider from that occurrence. A body keyed by provider alone names no occurrence. It requires `Content-Type: application/json`, since a body it cannot read is refused rather than taken for an empty one; the `events:write` scope; the right to edit THAT event, which is what decides — being a member of the agenda is neither sufficient nor always necessary; and ticketing enabled on the agenda, failing which it answers `422` `offers.disabled`. Its access token must travel in a header (`Authorization: Bearer …` or `access-token`) and NOT in the body: the endpoint reads its body only once it knows who is calling, so a token placed inside the body is never seen, where an ordinary v2 write would have taken it. ⚠️ READ ITS BODY, NOT ONLY ITS STATUS. It answers `200` with `{ success, applied, written, errors }`, and a batch whose occurrences it cannot resolve — an unknown or misspelled timing id — is a `200` carrying `success: false` and the faults in `errors`, having written nothing. A connector that checks the status alone reads a batch it never wrote as a success. AN ORDINARY EVENT WRITE CARRIES THIS INVENTORY. Whatever it sends for an occurrence the event already has is dropped before validation, so a read-modify-write round trip can neither change a row, nor add one, nor clear one with an explicit `[]` — and it is not critiqued field by field for rows that were never going to be written either. The reason is that such a write cannot be told apart from the inventory it merely transports: a connector writes between the moment a client reads an event and the moment its save arrives, so a row absent from that save may be one the client never saw rather than one it means to drop. The endpoint writes under a compare-and-swap and has no such ambiguity. A CLIENT SHOULD THEREFORE OMIT THE FIELD when saving an event it has just read: an occurrence whose entry does not mention `availability` keeps its stored rows. Omitting the FIELD is safe; omitting the OCCURRENCE is not — a timings write is a full replacement, and an occurrence no incoming entry claims is deleted along with its inventory. Sending the rows back is not merely useless, it is what pushes a read-modify-write past the 100 kB body limit of the ordinary event routes: measured, a payload crosses it at about 500 occurrences carrying one row each, or 300 carrying two. AN OCCURRENCE THE EVENT DOES NOT HAVE YET is the exception, and the only way an ordinary write stores inventory: it has no stored rows to protect, so the rows sent with it are kept — provided ticketing is enabled on the agenda, which is checked for exactly this case and answers `422` `offers.disabled` otherwise. An event can thus be created with its inventory in a single call. Served wherever a `Timing` is: `timings[]`, and also `firstTiming` / `lastTiming` / `nextTiming`, which copy the occurrence whole — so a compact listing carries the inventory of its next date. items: $ref: '#/components/schemas/TimingAvailability' TimingAvailability: type: object required: [provider, status, syncedAt] additionalProperties: false description: > What one ticketing provider says about one occurrence. `status` is the provider's word as of `syncedAt`; the sale window is what lets a reader tell `notYetOnSale` and `salesClosed` from a stock answer, and `offersAggregate` recomputes that half at read time. properties: provider: type: string enum: [ billetweb, eventbrite, helloasso, mapado, weezevent, passculture, manual, ] description: > Which ticketing source this row belongs to. `passculture` is the published spelling here; the `registration` service of the same name is spelled `passCulture` — two fields, two namespaces. status: type: string enum: [available, limited, soldOut, notYetOnSale, salesClosed, unknown] description: > `mixed` never appears here — it only exists on the aggregate, where occurrences may disagree. syncedAt: type: string format: date-time description: > When the connector READ this at the provider, with an explicit offset. A fact about the data, not about the feed: the event's `availabilityUpdatedAt` is set by the write, not by this stamp. Must not be in the future. sourceRef: type: string minLength: 1 maxLength: 255 description: > The provider's own key for this occurrence (a session, a stock, a series child). Opaque to OpenAgenda. onSaleFrom: type: string format: date-time onSaleThrough: type: string format: date-time description: Must be at or after `onSaleFrom` when both are set. closedBySource: type: boolean description: > Provenance of a `salesClosed`: `true` when the provider itself shut the channel, `false` or absent when only the sale window elapsed. Decides whether a later read may reopen the row on the window alone. OffersAggregate: type: [object, 'null'] additionalProperties: false description: > What the ticketing sources of this event add up to, computed on each read. `availability` is `null` when nothing can be said - every occurrence past, or no source carrying a usable status - and reflects the current time, except `soldOut` and `limited`, which reflect the last sync with the provider. `pricing` is a category rolled up from the sources' price lists and declared pricing. properties: availability: type: [string, 'null'] enum: [ available, limited, notYetOnSale, soldOut, salesClosed, unknown, mixed, null, ] description: > `mixed` means the occurrences disagree — one date sold out, another closed — and is a real answer rather than a failure to decide: a consumer should render it as "see dates". pricing: type: [string, 'null'] enum: [free, paid, donation, mixed, unknown, null] description: > A category, never an amount — the amounts are in `price`. `mixed` means the sources or the occurrences disagree (one date free, another paid); `unknown` means no source carries a usable pricing. price: allOf: - $ref: '#/components/schemas/OffersPrice' description: > The event's price bounds, over the same resolved tier sets as `pricing` — so the two can never disagree, and a `free` event cannot carry a non-zero minimum. links: type: [array, 'null'] description: > Where to book, one entry per ticketing source that carries a usable URL, in catalogue order. `null` when none does. The same URLs reach `registration` as `{type: link}` entries, but stripped of their source: that field lists ways to sign up, not a map of ticketing providers. A reader showing one line PER SOURCE — a per-date detail panel, say — could attach no URL to any line. This is that attribution, and nothing more. EVENT-LEVEL, NEVER PER DATE — a limit of what OpenAgenda stores and serves, not a claim about what ticketing services can do. An availability row cannot carry a URL (`TimingAvailability` is `additionalProperties: false`), and the only URL held for a source is the event-level one its connector wrote. A per-date link is constructible for at least some services — Mapado's booking site takes an `eventDate` parameter whose id is what a row's `sourceRef` is meant to carry — but building one per service belongs with the connectors, not here. items: $ref: '#/components/schemas/OffersLink' OffersLink: type: object additionalProperties: false required: [provider, url] properties: provider: type: string enum: [ billetweb, eventbrite, helloasso, mapado, weezevent, passculture, manual, ] url: type: string format: uri description: > The booking page this provider published for the event, cleaned by the same rule that decides what reaches `registration` — so the two never disagree on whether a source is linkable. OffersPrice: type: [object, 'null'] additionalProperties: false required: [minCents, maxCents, currency] description: > What the event's ticketing costs, in the smallest unit of its currency. ALL OR NOTHING. `null` whenever a single tier cannot be read as an amount - a membership at no stated price, a source that declared its pricing before pushing a catalogue - or when the contributing sources do not share one currency. A bound computed over part of a price list is wrong in both directions: the unreadable tier could sit below the minimum as easily as above the maximum. "Unknown" is an answer; "from EUR 18" when a EUR 9 tier exists is not. These bounds replace what a consumer was told to compute itself. T16 removed `priceRanges` on the grounds that "the tiers carry every price, and a consumer wanting 'from X' computes min(tiers[].priceCents)" - but the tiers are `read: ['internal']` and reach no public response, so that computation was never available to anyone. This is the only figure a public reader gets. properties: minCents: type: integer minimum: 0 description: > The cheapest way in. Zero on a free event, and on a pay-what-you-want tier that declares no floor. maxCents: type: [integer, 'null'] minimum: 0 description: > `null` means OPEN-ENDED, not "unknown": a pay-what-you-want tier has a floor and no ceiling, and publishing its floor as a ceiling would misstate what a visitor may give. currency: type: string description: > ISO 4217, upper-case. A source carries one currency; bounds are only served when every contributing source agrees on it. AgeRange: type: [object, 'null'] description: The audience age an event addresses. additionalProperties: false properties: min: type: [integer, 'null'] max: type: [integer, 'null'] Accessibility: type: [object, 'null'] description: > Per-facility accessibility flags — `true` when the event offers that facility. The keys are the `AccessibilityCode` values. additionalProperties: false properties: hi: type: boolean description: Hearing impairment. ii: type: boolean description: Intellectual impairment. vi: type: boolean description: Visual impairment. pi: type: boolean description: Psychic impairment. mi: type: boolean description: Motor impairment. Registration: type: object description: A single way for attendees to register. additionalProperties: false properties: type: type: string enum: [phone, email, link] value: type: string EnrichedLink: type: object additionalProperties: false properties: link: type: string format: uri data: type: object description: Optional enrichment metadata extracted from the link. additionalProperties: true ImageRef: type: object description: > A lightweight image for an embedded reference (an agenda logo on an event's originAgenda/sourceAgendas, or a provenance facet bucket): a ready-to-use `src` plus the `{geo}` template. The agenda's own `image` carries the full `Image`. additionalProperties: false required: [src, srcTemplate] properties: src: type: [string, 'null'] format: uri description: > A ready-to-use default rendition URL (a modest proportional width) — a naive `` just works. srcTemplate: type: [string, 'null'] description: > A URL with a literal `{geo}` placeholder to substitute with a geometry, to render the ref image at any size. Supported geometries: `{W}x{H}`, where `0` on an axis means proportional to the other (e.g. `800x0`); append `/smart` to crop on the detected point of interest rather than the centre, or prefix `fit-in/` to contain the image instead of cropping it. `null` when no rendition can be derived - use `src` as is. AgendaRef: type: object description: A compact reference to an agenda. additionalProperties: false properties: uid: type: integer format: int64 title: type: [string, 'null'] slug: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/ImageRef' - type: 'null' url: type: [string, 'null'] format: uri official: type: [boolean, 'null'] SourceAgendaRef: type: object description: > A source-agenda reference as carried by provenance facet buckets. additionalProperties: false required: [uid] properties: uid: type: integer format: int64 title: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/ImageRef' - type: 'null' ExtId: type: object description: An external identifier mapping this event to a third-party system. additionalProperties: false properties: key: type: string value: type: string AdditionalFields: type: object description: > Agenda-specific additional fields. The available keys and the shape of each value are defined by the agenda's event form schema (`GET /agendas/{agendaUid}/events/schema`): each value follows its field's `fieldType`. On a write, a key the schema does not declare answers `422`. A `file`/`image` field is set by reference, like the native image: stage the bytes via `POST /agendas/{uid}/uploads`, then send `{ ref, name? }` here (`name` is the original filename, used for the download; optional), or `null` to clear it. On read the same field returns the stored descriptor `{ originalName, extension, filename }`, where `filename` is the stored object's key; the API does not return its absolute URL. additionalProperties: x-additionalPropertiesName: fieldName AgendaNetworkRef: type: [object, 'null'] description: A compact reference to the network an agenda belongs to, or `null`. additionalProperties: false required: [uid, title] properties: uid: type: integer format: int64 title: type: [string, 'null'] AgendaLocationSetRef: type: [object, 'null'] description: A compact reference to the location set bound to an agenda, or `null`. additionalProperties: false required: [uid, title] properties: uid: type: integer format: int64 title: type: [string, 'null'] AgendaSummary: type: object description: > Base agenda representation returned by the list endpoint by default (`detailed: false`) - identity and display essentials only. additionalProperties: false required: - uid - slug - title - description - image - official example: uid: 12345 slug: festival-des-lumieres title: Festival des Lumières description: La programmation officielle du Festival des Lumières. image: width: 1400 height: 900 src: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/12345.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/12345.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg official: true properties: uid: type: integer format: int64 slug: type: string title: type: string description: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' official: type: boolean description: Whether the agenda is an official one. AgendaDetailed: type: object description: > Detailed agenda representation returned by the list endpoint when `detailed: true`. `url`, `updatedAt`, `officializedAt`, `private` and `indexed` are returned by the single-agenda get only. additionalProperties: false required: - uid - slug - title - description - image - official - createdAt - network - locationSet properties: uid: type: integer format: int64 slug: type: string title: type: string description: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' official: type: boolean description: Whether the agenda is an official one. createdAt: type: string format: date-time network: $ref: '#/components/schemas/AgendaNetworkRef' locationSet: $ref: '#/components/schemas/AgendaLocationSetRef' Agenda: type: object description: > Full agenda representation returned by the single-agenda get. additionalProperties: false required: - uid - slug - title - description - image - official - url - createdAt - updatedAt - officializedAt - private - indexed - network - locationSet example: uid: 12345 slug: festival-des-lumieres title: Festival des Lumières description: La programmation officielle du Festival des Lumières. image: width: 1400 height: 900 src: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/12345.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/12345.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/12345.full.image.jpg official: true url: https://openagenda.com/festival-des-lumieres createdAt: '2024-11-04T09:12:00+01:00' updatedAt: '2025-05-28T16:45:00+02:00' officializedAt: '2024-11-15T10:00:00+01:00' private: false indexed: true network: uid: 87 title: Ville de Lyon locationSet: uid: 204 title: Lieux de Lyon properties: uid: type: integer format: int64 slug: type: string title: type: string description: type: [string, 'null'] image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' official: type: boolean description: Whether the agenda is an official one. url: type: [string, 'null'] format: uri description: Public website URL of the agenda, or `null`. createdAt: type: string format: date-time updatedAt: type: string format: date-time officializedAt: type: [string, 'null'] format: date-time description: When the agenda was officialized, or `null` if it is not official. private: type: boolean description: Whether the agenda is private (not publicly listed). indexed: type: boolean description: Whether the agenda is indexed in public search. network: $ref: '#/components/schemas/AgendaNetworkRef' locationSet: $ref: '#/components/schemas/AgendaLocationSetRef' AgendaList: type: object required: [data, pagination] additionalProperties: false properties: data: type: array description: > `AgendaSummary` items by default, or `AgendaDetailed` items when `detailed=true`. items: # Detailed branch first — see LocationList for why. oneOf: - $ref: '#/components/schemas/AgendaDetailed' - $ref: '#/components/schemas/AgendaSummary' pagination: $ref: '#/components/schemas/Pagination' # The two visibility scopes of an agenda's events share a metric vocabulary # (scalar totals, `by` maps of `key → count`, a spatial `viewport`, # a thematic `keywords` list) but are DISTINCT schemas, each fully-required so # the SDK types every field as present (not `T | undefined`). They differ only # by their scope-specific keys: `published` carries the exact distinct counts # `locations`/`creators`; `all` carries the per-moderation-state `byState`. # The whole `all` scope is the single optional — it is gated by access (see # AgendaOverview), so a consumer reasons about its presence from the access it # holds, while every field WITHIN a returned scope is guaranteed. PublishedEventStats: type: object additionalProperties: false description: > Statistics over an agenda's **publicly visible** events (`events.published`), including the exact distinct counts `locations` and `creators`. required: - total - locations - creators - timeline - byLanguage - bySource - byEndDay - laterDays - viewport - keywords example: total: 28727 locations: 456 creators: 79 timeline: { current: 27, passed: 7, upcoming: 28693 } byLanguage: { fr: 28100, en: 627 } bySource: { contribution: 879, shared: 0, aggregation: 27848 } byEndDay: { '2026-06-17': 12, '2026-06-18': 30 } laterDays: 28685 viewport: topLeft: { latitude: 51.09, longitude: -5.14 } bottomRight: { latitude: 41.33, longitude: 9.56 } keywords: [Paris, Théâtre, Culture] properties: total: type: integer description: Total number of published events. locations: type: integer description: Distinct published-event locations (exact). creators: type: integer description: Distinct published-event creators (exact). timeline: type: object additionalProperties: false description: Event counts relative to now. required: [current, passed, upcoming] properties: current: type: integer description: Events happening now (started, not yet ended). passed: type: integer description: Events already ended. upcoming: type: integer description: Events not yet started. byLanguage: type: object description: Event count per language code. additionalProperties: type: integer bySource: type: object description: > Event count per add method (`contribution`, `shared`, `aggregation`). The three known methods are always present (0 when absent). additionalProperties: type: integer byEndDay: type: object description: > Per-day count of events ending within the next 30 days, keyed by `YYYY-MM-DD`. Use `laterDays` for the residual past that horizon. additionalProperties: type: integer laterDays: type: integer description: Count of events ending past the 30-day `byEndDay` horizon. viewport: oneOf: - $ref: '#/components/schemas/Viewport' - type: 'null' description: > Bounding box enclosing this scope's events with coordinates, or `null` when none has any. keywords: type: array description: > De-duplicated thematic terms (cities, departments, regions and keywords) characterising this scope's events. items: type: string AllEventStats: type: object additionalProperties: false description: > Statistics over an agenda's events of **any moderation state** (moderation and refused included) - `events.all`, with the per-moderation-state breakdown `byState`. required: - total - byState - timeline - byLanguage - bySource - byEndDay - laterDays - viewport - keywords example: total: 1248000 byState: { '-1': 5000, '1': 200000, '2': 1043000 } timeline: { current: 30, passed: 900000, upcoming: 348000 } byLanguage: { fr: 1200000, en: 48000 } bySource: { contribution: 50000, shared: 12, aggregation: 1198000 } byEndDay: { '2026-06-17': 40 } laterDays: 347900 viewport: null keywords: [Lyon, Concert] properties: total: type: integer description: Total number of events across all moderation states. byState: type: object description: > Event count per moderation state (`ModerationState` codes as keys). additionalProperties: type: integer timeline: type: object additionalProperties: false description: Event counts relative to now. required: [current, passed, upcoming] properties: current: type: integer description: Events happening now (started, not yet ended). passed: type: integer description: Events already ended. upcoming: type: integer description: Events not yet started. byLanguage: type: object description: Event count per language code. additionalProperties: type: integer bySource: type: object description: > Event count per add method (`contribution`, `shared`, `aggregation`). The three known methods are always present (0 when absent). additionalProperties: type: integer byEndDay: type: object description: > Per-day count of events ending within the next 30 days, keyed by `YYYY-MM-DD`. Use `laterDays` for the residual past that horizon. additionalProperties: type: integer laterDays: type: integer description: Count of events ending past the 30-day `byEndDay` horizon. viewport: oneOf: - $ref: '#/components/schemas/Viewport' - type: 'null' description: > Bounding box enclosing this scope's events with coordinates, or `null` when none has any. keywords: type: array description: > De-duplicated thematic terms (cities, departments, regions and keywords) characterising this scope's events. items: type: string RecentlyAddedStats: type: object additionalProperties: false description: > Published events whose `originAgenda` is this agenda, added within the last `window` days and with an occurrence still to begin, counted by add method. Identical for every caller. required: [window, bySource] example: window: 14 bySource: { contribution: 12, shared: 0, aggregation: 3 } properties: window: type: integer description: > Width of the slice in days. bySource: type: object description: > Event count per add method (`contribution`, `shared`, `aggregation`). additionalProperties: type: integer AgendaOverview: type: object additionalProperties: false description: > Activity overview of one agenda, computed on each request: event volume, distributions, spatial extent and thematic keywords per visibility scope (`events.published` / `events.all`), plus the `recentlyAdded` slice. `events.published` (`PublishedEventStats`) is always present; `events.all` (`AllEventStats`) is present only for `administrator|moderator|internal` callers. required: [events, recentlyAdded] properties: events: type: object additionalProperties: false required: [published] properties: published: $ref: '#/components/schemas/PublishedEventStats' all: $ref: '#/components/schemas/AllEventStats' recentlyAdded: $ref: '#/components/schemas/RecentlyAddedStats' # Shared enum types — one home for the values AND their meaning # (x-enum-descriptions, rendered per-value by Scalar and surfaced by the MCP). # $ref'd from both the response models and the matching filter parameters, so # the labels never drift between the two. EventStatus: type: integer enum: [1, 2, 3, 4, 5, 6] description: Lifecycle status of the event. x-enum-descriptions: '1': Scheduled '2': Rescheduled '3': Moved online '4': Postponed '5': Full '6': Cancelled AttendanceMode: type: integer enum: [1, 2, 3] description: How attendees take part. x-enum-descriptions: '1': Offline (on-site) '2': Online '3': Mixed (on-site and online) ModerationState: type: integer enum: [-2, -1, 0, 1, 2] description: Moderation state of the event. x-enum-descriptions: '-2': Removed '-1': Refused '0': To control '1': Controlled '2': Published AccessibilityCode: type: string enum: [hi, ii, mi, pi, vi] description: Accessibility facility code. x-enum-descriptions: hi: Hearing impairment ii: Intellectual impairment mi: Motor impairment pi: Psychic impairment vi: Visual impairment EventSummary: type: object description: > Compact event representation returned by the list endpoint (`detailed: false`); the single-event get and `detailed=true` return the full `Event`. The compact view exposes the occurrence span through `firstTiming`/`lastTiming`/`nextTiming`, interpreted in `timezone`; fetch a single event for the full list of occurrences. additionalProperties: false required: - uid - slug - title - description - status - dateRange - featured - image - imageCredits - keywords - originAgenda - location - timezone - attendanceMode - onlineAccessLink - firstTiming - lastTiming - nextTiming - additionalFields example: uid: 9876543 slug: concert-de-jazz-au-parc title: { fr: Concert de jazz au parc, en: Jazz concert in the park } description: { fr: Une soirée jazz en plein air., en: An open-air jazz evening. } status: 1 dateRange: { fr: 'Le 20 juin 2025 de 18h00 à 22h00', en: 'June 20, 2025, 6:00 PM – 10:00 PM', } featured: false image: width: 1200 height: 800 src: https://img.openagenda.com/u/1080x0/main/9876543.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/9876543.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/9876543.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/9876543.full.image.jpg imageCredits: Ville de Lyon keywords: { fr: [jazz, concert, plein air], en: [jazz, concert, open air] } originAgenda: uid: 12345 title: Festival des Lumières slug: festival-des-lumieres image: src: https://img.openagenda.com/u/400x0/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg url: https://openagenda.com/festival-des-lumieres official: true location: uid: 55512 name: Parc de la Tête d'Or address: Place Général Leclerc city: Lyon region: Auvergne-Rhône-Alpes postalCode: '69006' countryCode: FR latitude: 45.7772 longitude: 4.8558 district: null department: Rhône adminLevel3: null adminLevel5: null country: { fr: France, en: France } timezone: Europe/Paris timezone: Europe/Paris attendanceMode: 1 onlineAccessLink: null firstTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } lastTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } nextTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } additionalFields: {} properties: uid: type: integer format: int64 readOnly: true slug: type: string readOnly: true title: $ref: '#/components/schemas/LocalizedString' description: $ref: '#/components/schemas/LocalizedString' status: $ref: '#/components/schemas/EventStatus' dateRange: allOf: - $ref: '#/components/schemas/LocalizedString' readOnly: true description: Human-readable, localized date range computed from the timings. featured: type: boolean readOnly: true image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' imageCredits: type: [string, 'null'] maxLength: 255 keywords: $ref: '#/components/schemas/LocalizedStringArray' originAgenda: readOnly: true description: The agenda the event was originally created in. oneOf: - $ref: '#/components/schemas/AgendaRef' - type: 'null' location: oneOf: - $ref: '#/components/schemas/EventLocation' - type: 'null' timezone: type: [string, 'null'] readOnly: true description: IANA time zone the timings are expressed in (null when unset). attendanceMode: $ref: '#/components/schemas/AttendanceMode' onlineAccessLink: type: [string, 'null'] format: uri firstTiming: readOnly: true oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' lastTiming: readOnly: true oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' nextTiming: readOnly: true description: > The first occurrence that has not ended yet (an occurrence in progress included), or `null` when every occurrence has ended. oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' additionalFields: $ref: '#/components/schemas/AdditionalFields' Event: type: object description: > Full event representation returned by the single-event get, the writes, and the list when `detailed=true`. It carries the EventSummary base field set plus the detailed-only fields. additionalProperties: false required: # base (EventSummary) fields - uid - slug - title - description - status - dateRange - featured - image - imageCredits - keywords - originAgenda - timings - location - timezone - attendanceMode - onlineAccessLink - firstTiming - lastTiming - nextTiming - additionalFields # detailed-only fields - longDescription - conditions - country - registration - createdAt - updatedAt - accessibility - age - state - links - extIds - sourceAgendas example: uid: 9876543 slug: concert-de-jazz-au-parc title: { fr: Concert de jazz au parc, en: Jazz concert in the park } description: { fr: Une soirée jazz en plein air., en: An open-air jazz evening. } status: 1 dateRange: { fr: 'Le 20 juin 2025 de 18h00 à 22h00', en: 'June 20, 2025, 6:00 PM – 10:00 PM', } featured: false image: width: 1200 height: 800 src: https://img.openagenda.com/u/1080x0/main/9876543.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/9876543.full.image.jpg srcset: - width: 640 url: https://img.openagenda.com/u/640x0/main/9876543.full.image.jpg - width: 1080 url: https://img.openagenda.com/u/1080x0/main/9876543.full.image.jpg imageCredits: Ville de Lyon keywords: { fr: [jazz, concert, plein air], en: [jazz, concert, open air] } originAgenda: uid: 12345 title: Festival des Lumières slug: festival-des-lumieres image: src: https://img.openagenda.com/u/400x0/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg url: https://openagenda.com/festival-des-lumieres official: true timings: - begin: '2025-06-20T18:00:00+02:00' end: '2025-06-20T22:00:00+02:00' location: uid: 55512 name: Parc de la Tête d'Or address: Place Général Leclerc city: Lyon region: Auvergne-Rhône-Alpes postalCode: '69006' countryCode: FR latitude: 45.7772 longitude: 4.8558 district: null department: Rhône adminLevel3: null adminLevel5: null country: { fr: France, en: France } timezone: Europe/Paris timezone: Europe/Paris attendanceMode: 1 onlineAccessLink: null firstTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } lastTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } nextTiming: { begin: '2025-06-20T18:00:00+02:00', end: '2025-06-20T22:00:00+02:00', } additionalFields: {} longDescription: { fr: 'Un concert en plein air avec plusieurs formations de jazz…', en: 'An open-air concert featuring several jazz ensembles…', } conditions: { fr: Entrée libre, en: Free entry } country: { fr: France, en: France } registration: - type: link value: https://openagenda.com/festival-des-lumieres/events/concert-de-jazz-au-parc createdAt: '2025-03-12T11:30:00+01:00' updatedAt: '2025-05-28T16:45:00+02:00' accessibility: { hi: false, ii: false, vi: true, pi: false, mi: true } age: { min: 0, max: null } state: 2 links: - link: https://example.org/jazz-au-parc data: {} extIds: - key: legacy value: '4421007' sourceAgendas: [] properties: uid: type: integer format: int64 readOnly: true slug: type: string readOnly: true description: > Unique across all events. Set by the server from the title when the event is created, and never changed afterwards, even if the title changes. title: $ref: '#/components/schemas/LocalizedString' description: $ref: '#/components/schemas/LocalizedString' status: $ref: '#/components/schemas/EventStatus' dateRange: allOf: - $ref: '#/components/schemas/LocalizedString' readOnly: true description: Human-readable, localized date range computed from the timings. featured: type: boolean readOnly: true image: oneOf: - $ref: '#/components/schemas/Image' - type: 'null' imageCredits: type: [string, 'null'] maxLength: 255 keywords: $ref: '#/components/schemas/LocalizedStringArray' originAgenda: readOnly: true description: The agenda the event was originally created in. oneOf: - $ref: '#/components/schemas/AgendaRef' - type: 'null' timings: type: array items: $ref: '#/components/schemas/Timing' location: oneOf: - $ref: '#/components/schemas/EventLocation' - type: 'null' timezone: type: [string, 'null'] readOnly: true description: IANA time zone the timings are expressed in (null when unset). attendanceMode: $ref: '#/components/schemas/AttendanceMode' onlineAccessLink: type: [string, 'null'] format: uri firstTiming: readOnly: true oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' lastTiming: readOnly: true oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' nextTiming: readOnly: true description: > The first occurrence that has not ended yet (an occurrence in progress included), or `null` when every occurrence has ended. oneOf: - $ref: '#/components/schemas/Timing' - type: 'null' additionalFields: $ref: '#/components/schemas/AdditionalFields' longDescription: $ref: '#/components/schemas/LocalizedString' conditions: $ref: '#/components/schemas/LocalizedString' country: readOnly: true description: Localized country label of the event location. oneOf: - $ref: '#/components/schemas/LocalizedString' - type: 'null' registration: type: array items: $ref: '#/components/schemas/Registration' offersAggregate: allOf: - $ref: '#/components/schemas/OffersAggregate' readOnly: true description: > Present only on an event with a ticketing catalogue. createdAt: type: string format: date-time readOnly: true updatedAt: type: string format: date-time readOnly: true availabilityUpdatedAt: type: string format: date-time readOnly: true description: > A SECOND CURSOR, for inventory rather than catalogue. An availability write does not move `updatedAt`: an inventory tick is not a content change, and a catalogue consumer paging on `updatedAt.asc` must not see an event resurface every time a seat is sold. This stamp moves instead, so availability stays followable on its own — `?sort=availabilityUpdatedAt.asc&availabilityUpdatedAt[gte]=`. Its value is the instant OpenAgenda last WROTE an availability row of this event — the write clock. It is not the rows' own `syncedAt`, which says when each provider was read and may sit behind another provider's row, so a max-of-`syncedAt` would not move forward at all for such a provider. THE STAMP IS TAKEN AT THE WRITE, THE DOCUMENT IS INDEXED AFTER, and the two orders can differ: an event stamped at t1 may be indexed after one stamped at t2 > t1, and a consumer that has already moved its watermark to t2 would never see it. Re-indexing is also best-effort — a failed resync is retried later, not at the instant of the write. So overlap rather than trust exhaustiveness: page with a watermark set a little behind the newest value you saw (a few minutes covers the ordinary case), and make your ingestion idempotent. Rows carry `syncedAt`, so re-reading one costs nothing. ABSENT from an event that has never received availability, which is the overwhelming majority. A `[gte]` filter therefore excludes them naturally, which is the intent: following inventory should not mean paging through the whole corpus. IT DOES NOT MOVE WHEN AN EVENT LEAVES. Deleting an event, or unpublishing it, is a catalogue change: it stamps `updatedAt` and leaves this one where it was. A consumer that follows inventory ALONE therefore keeps selling dates for an event that is gone — follow `updatedAt` as well, or re-read the events you hold, to see departures. accessibility: $ref: '#/components/schemas/Accessibility' age: $ref: '#/components/schemas/AgeRange' state: allOf: - $ref: '#/components/schemas/ModerationState' readOnly: true links: type: array readOnly: true items: $ref: '#/components/schemas/EnrichedLink' extIds: type: array items: $ref: '#/components/schemas/ExtId' sourceAgendas: type: array readOnly: true description: Agendas this event was aggregated from. items: $ref: '#/components/schemas/AgendaRef' UploadTicket: type: object additionalProperties: false required: [ref, expiresAt] description: > A staged upload. Attach `ref` on an event write (`image`, or a custom file/image field under `additionalFields`) before it expires. properties: ref: type: string description: The staging reference to attach on an event write. expiresAt: type: string format: date-time description: > The staged upload is attachable at least until this instant (ISO 8601); attaching it later may answer `422`. UploadDescriptor: type: object additionalProperties: false required: [uploadUrl, method, header, field, ticket, maxBytes, expiresAt] description: > An out-of-band upload authorization. `POST` the raw file to `uploadUrl` with a plain HTTPS request, sending `ticket` in the `header` header and the file as multipart `field`. That call returns an `UploadTicket` whose `ref` you attach with `image: { ref }`. properties: uploadUrl: type: string format: uri description: Absolute URL to POST the file to (out of band). method: type: string description: The HTTP method to use for the upload (`POST`). header: type: string description: The header carrying the `ticket` (`X-Upload-Ticket`). field: type: string description: The multipart form field for the file (`file`). ticket: type: string description: > The single-use capability ticket. Upload-only, bound to this agenda, one file, short-lived; carries no account credential. maxBytes: type: integer description: Maximum accepted file size, in bytes. expiresAt: type: string format: date-time description: When the ticket expires (ISO 8601). ImageInput: description: > Write shape for an event image (the read `Image` carries the rendition URLs). Attach the image one of two ways - a freshly-staged upload by its `ref` (the value returned by `POST /agendas/{uid}/uploads`), or a publicly reachable `url` the server fetches - or send `null` to remove the current image. The image is stored as part of the same write as the rest of the edit; renditions are derived from it on demand. The `url` must be a publicly reachable `http(s)` image, without credentials in the URL. A URL that is malformed, not `http(s)`, carries credentials, is not publicly reachable, cannot be retrieved, is not a valid image, or is larger than 20 MiB is rejected with `422`. An object carrying both `ref` and `url`, or any other key, is rejected with `400`. oneOf: - type: object additionalProperties: false required: [ref] properties: ref: type: string description: > A staging reference returned by the upload endpoint (of the form `staging/{agendaUid}/…`). It must belong to this agenda - a reference for another agenda answers `422` - and be attached before the upload's `expiresAt`; attaching it later may answer `422`. - type: object additionalProperties: false required: [url] properties: url: type: string format: uri maxLength: 2048 description: > A publicly reachable `http(s)` image URL, without embedded credentials. The server retrieves it and stores it like a staged upload. - type: 'null' EventInput: type: object description: > Request body for creating or replacing an event: the fields a client may set. Agenda-specific fields go under `additionalFields`, never at the top level; any other top-level key (a read-only field such as `uid` or `slug`, an unknown name) is rejected with `400`, as is an `additionalFields` name that collides with a native field. Field values are validated by the server (a `422` with per-field `error.errors[]` on failure). Required fields depend on the target agenda: an event needs at least `title`, `description` and `timings`, plus a `location` unless it is online-only, plus whatever the agenda marks required; a missing one answers `422` with the per-field set under `error.errors[]`. The `image` is set either by reference - stage the bytes via `POST /agendas/{uid}/uploads` and pass the returned `ref` here - or by a public `url` the server fetches (or `null` to clear it). An event's privacy is derived from its agenda. additionalProperties: false properties: title: allOf: - $ref: '#/components/schemas/LocalizedString' description: > When the event is created, its `slug` is derived from this title, preferring the language that comes first in the request body. Put the event's original language first. description: $ref: '#/components/schemas/LocalizedString' longDescription: $ref: '#/components/schemas/LocalizedString' conditions: $ref: '#/components/schemas/LocalizedString' keywords: $ref: '#/components/schemas/LocalizedStringArray' timings: type: array description: > The event's occurrences. Required for a publishable event; the server enforces the exact requirement for the target agenda. items: $ref: '#/components/schemas/Timing' location: description: > The event's location, referenced by its uid; a uid that matches no location answers `422`. oneOf: - $ref: '#/components/schemas/EventLocationRef' - type: 'null' attendanceMode: $ref: '#/components/schemas/AttendanceMode' onlineAccessLink: type: [string, 'null'] format: uri description: > Access link for an online or hybrid event (`attendanceMode` `2` or `3`). accessibility: $ref: '#/components/schemas/Accessibility' age: $ref: '#/components/schemas/AgeRange' registration: type: array description: > Ways for attendees to register; an invalid entry answers `422`. items: $ref: '#/components/schemas/Registration' extIds: type: array description: > External identifiers mapping this event to your own system(s). Use these to resolve the event later via the by-ext-id endpoints. items: $ref: '#/components/schemas/ExtId' state: allOf: - $ref: '#/components/schemas/ModerationState' description: > Moderation state, arbitrated by the server. A moderator or administrator's value is applied; requesting `2` (published) without publish permission answers `403`. For other members, on creation the value is ignored and the agenda's default contribution state applies; on update, the agenda's moderation settings decide whether the existing state is kept or reset to `0` (to control). Omit it to let the agenda's settings decide. status: allOf: - $ref: '#/components/schemas/EventStatus' description: > Event lifecycle status. Accepted only on agendas that have the event status feature enabled; otherwise sending it answers `422`. image: $ref: '#/components/schemas/ImageInput' imageCredits: type: [string, 'null'] maxLength: 255 description: > Credits for the event `image`. Ignored when the event has no image; `null` clears them. additionalFields: $ref: '#/components/schemas/AdditionalFields' EventPatch: type: object description: > Partial update body: every field is optional, and only the provided fields are changed. additionalProperties: false properties: title: allOf: - $ref: '#/components/schemas/LocalizedString' description: > When the event is created, its `slug` is derived from this title, preferring the language that comes first in the request body. Put the event's original language first. description: $ref: '#/components/schemas/LocalizedString' longDescription: $ref: '#/components/schemas/LocalizedString' conditions: $ref: '#/components/schemas/LocalizedString' keywords: $ref: '#/components/schemas/LocalizedStringArray' timings: type: array items: $ref: '#/components/schemas/Timing' location: oneOf: - $ref: '#/components/schemas/EventLocationRef' - type: 'null' attendanceMode: $ref: '#/components/schemas/AttendanceMode' onlineAccessLink: type: [string, 'null'] format: uri accessibility: $ref: '#/components/schemas/Accessibility' age: $ref: '#/components/schemas/AgeRange' registration: type: array items: $ref: '#/components/schemas/Registration' extIds: type: array items: $ref: '#/components/schemas/ExtId' state: allOf: - $ref: '#/components/schemas/ModerationState' description: > Moderation state, arbitrated by the server. A moderator or administrator's value is applied; requesting `2` (published) without publish permission answers `403`. For other members, the agenda's moderation settings decide whether the existing state is kept or reset to `0` (to control). status: allOf: - $ref: '#/components/schemas/EventStatus' description: > Event lifecycle status. Accepted only on agendas that have the event status feature enabled; otherwise sending it answers `422`. image: $ref: '#/components/schemas/ImageInput' imageCredits: type: [string, 'null'] maxLength: 255 description: > Credits for the event `image`. Ignored when the event has no image; `null` clears them. additionalFields: $ref: '#/components/schemas/AdditionalFields' EventLocationRef: type: object description: A reference to an existing location by its OpenAgenda uid. additionalProperties: false required: [uid] properties: uid: type: integer format: int64 minimum: 1 DeletionResult: type: object description: > Confirmation returned by a delete: the uid of the removed resource and a `deleted: true` marker. additionalProperties: false required: [uid, deleted] properties: uid: type: integer format: int64 deleted: type: boolean enum: [true] EventList: type: object required: [data, pagination] additionalProperties: false properties: data: type: array description: > `EventSummary` items by default, or full `Event` items when `detailed=true` is passed. items: # Detailed branch first — see LocationList for why. oneOf: - $ref: '#/components/schemas/Event' - $ref: '#/components/schemas/EventSummary' pagination: $ref: '#/components/schemas/Pagination' FacetName: type: string description: A facet type name (shared by the GET `facets` list and the POST report). enum: - additionalFields - additionalFieldMetrics - cities - regions - departments - districts - countryCodes - keywords - languages - accessibilities - status - attendanceModes - originAgendas - sourceAgendas - locations - geohash - viewport - timespan - timings - dateRanges FacetSpec: type: object required: [type] additionalProperties: false description: > A single named aggregation in a facet report. `name` (the output key) defaults to `type`; reuse a `type` under distinct `name`s to aggregate the same field several ways in one request. Options not relevant to the `type` are ignored. properties: name: type: string description: > Output key (alias) the result is returned under. Defaults to `type`. A duplicate name answers `400`. type: $ref: '#/components/schemas/FacetName' size: type: integer minimum: 1 maximum: 250 description: Max buckets (bucket-list families). Overrides `facetSize`. sort: type: string enum: [count, alpha] description: Bucket order. Overrides `facetSort`. missing: type: string description: > Label for a synthetic bucket counting events with no value for the field (value-keyed term facets only; ignored elsewhere). zoom: type: integer minimum: 1 description: Clustering zoom for the `geohash` facet. interval: type: string enum: [hour, day, week, month, year] description: Bucket width for the `timings` date histogram. month: type: string description: Calendar month (`YYYY-MM`) for the `dateRanges` facet. fields: type: array items: { type: string } description: > For `additionalFields`/`additionalFieldMetrics`: which agenda fields to aggregate (default: all readable fields of the matching type). FacetReportRequest: type: object required: [facets] additionalProperties: false description: Body of the named-aggregations facet report (`POST .../events/facets`). example: filters: { keyword: [concert] } facets: - cities - { name: topKeywords, type: keywords, size: 20, sort: count } - { name: byMonth, type: timings, interval: month } - { name: byYear, type: timings, interval: year } properties: filters: $ref: '#/components/schemas/EventFilters' facetSize: type: integer minimum: 1 maximum: 250 default: 10 description: Request-wide default bucket size; a facet's own `size` overrides it. facetSort: type: string enum: [count, alpha] default: count description: Request-wide default order; a facet's own `sort` overrides it. facets: type: array minItems: 1 description: > The aggregations to compute. Each item is either a bare facet name (simple form) or a `FacetSpec` object (named, with options). items: oneOf: - $ref: '#/components/schemas/FacetName' - $ref: '#/components/schemas/FacetSpec' EventFilters: type: object description: > Filters scoping the facet report - the event list filters, in their native JSON types. Unknown keys are ignored. properties: search: { type: string } uid: { type: array, items: { type: integer } } slug: { type: array, items: { type: string } } extId: type: object properties: { key: { type: string }, value: { type: string } } keyword: { type: array, items: { type: string } } language: { type: array, items: { type: string } } accessibility: type: array items: { type: string, enum: [hi, ii, mi, pi, vi] } status: { type: array, items: { type: integer, enum: [1, 2, 3, 4, 5, 6] } } attendanceMode: { type: array, items: { type: integer, enum: [1, 2, 3] } } featured: { type: boolean } locationUid: { type: array, items: { type: integer } } locationExtId: type: object properties: { key: { type: string }, value: { type: string } } region: { type: array, items: { type: string } } department: { type: array, items: { type: string } } city: { type: array, items: { type: string } } district: { type: array, items: { type: string } } adminLevel3: { type: array, items: { type: string } } adminLevel5: { type: array, items: { type: string } } countryCode: { type: array, items: { type: string } } bbox: { type: string, description: '"west,south,east,north" in decimal degrees.', } near: { type: string, description: '"lat,lng" — requires `radius`.' } radius: { type: integer, description: Metres; requires `near`. } originAgendaUid: { type: array, items: { type: integer } } originAgendaOfficial: { type: boolean } sourceAgendaUid: { type: array, items: { type: integer } } relative: type: array items: { type: string, enum: [passed, upcoming, current] } timings: { $ref: '#/components/schemas/DateRangeFilter' } createdAt: { $ref: '#/components/schemas/DateRangeFilter' } updatedAt: { $ref: '#/components/schemas/DateRangeFilter' } # Le curseur d'inventaire est accepté ici comme sur le listing : les # deux routes de facettes passent par `buildEventSearchQuery`, qui le # lit depuis ce lot. Déclaré dans la foulée, sinon les deux surfaces # divergent — le filtre marcherait sans être écrit nulle part. availabilityUpdatedAt: { $ref: '#/components/schemas/DateRangeFilter' } localTime: type: object properties: { gte: { type: integer }, lte: { type: integer } } age: type: object properties: { gte: { type: integer }, lte: { type: integer } } sort: { type: string } additionalFields: type: object description: Per-field filters, keyed by agenda field name. DateRangeFilter: type: object description: A date range; bounds are RFC 3339 (date or date-time with offset). properties: gte: { type: string } lte: { type: string } FacetReport: type: object required: [facets] additionalProperties: false description: Named facet results, keyed by each facet's `name` alias. example: facets: cities: { type: cities, result: [{ value: Lyon, count: 42 }] } byMonth: type: timings result: [{ value: '2025-06', count: 18 }, { value: '2025-07', count: 31 }] properties: facets: type: object description: One entry per requested facet, keyed by its `name` alias. additionalProperties: $ref: '#/components/schemas/FacetReportEntry' FacetReportEntry: description: > A facet result tagged with its `type`. The `type` tells the client how to read `result` (a bucket array, a viewport/timespan object or `null`, or a per-field map). oneOf: - type: object required: [type, result] additionalProperties: false properties: type: type: string enum: [ cities, regions, departments, districts, countryCodes, keywords, languages, accessibilities, status, attendanceModes, timings, dateRanges, ] result: type: array items: { $ref: '#/components/schemas/FacetBucket' } - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [originAgendas] } result: type: array items: { $ref: '#/components/schemas/AgendaFacetBucket' } - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [sourceAgendas] } result: type: array items: { $ref: '#/components/schemas/SourceAgendaFacetBucket' } - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [locations] } result: type: array items: { $ref: '#/components/schemas/LocationFacetBucket' } - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [geohash] } result: type: array items: { $ref: '#/components/schemas/GeoFacetBucket' } - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [viewport] } result: oneOf: - $ref: '#/components/schemas/Viewport' - type: 'null' - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [timespan] } result: oneOf: - $ref: '#/components/schemas/Timespan' - type: 'null' - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [additionalFields] } result: type: object additionalProperties: $ref: '#/components/schemas/AdditionalFieldFacet' - type: object required: [type, result] additionalProperties: false properties: type: { type: string, enum: [additionalFieldMetrics] } result: type: object additionalProperties: $ref: '#/components/schemas/AdditionalFieldMetricsFacet' FacetBucket: type: object description: >- One value of a facet, with the number of events that carry it. required: [value, count] additionalProperties: false properties: value: type: string description: > The facet value — e.g. a city name, a keyword, a language code, a status. Always a string (numeric-keyed facets are stringified). count: type: integer description: Number of events in the filtered set with this value. FacetResults: type: object description: The buckets computed for each facet the request asked for. required: [facets] additionalProperties: false example: facets: cities: - { value: Lyon, count: 42 } - { value: Villeurbanne, count: 11 } keywords: - { value: jazz, count: 18 } - { value: concert, count: 57 } status: - { value: '1', count: 80 } - { value: '6', count: 7 } attendanceModes: - { value: '1', count: 74 } - { value: '2', count: 13 } originAgendas: - agenda: uid: 12345 title: Festival des Lumières slug: festival-des-lumieres image: src: https://img.openagenda.com/u/400x0/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg srcTemplate: https://img.openagenda.com/u/{geo}/main/a1b2c3d4e5f60718293a4b5c6d7e8f90.full.image.jpg url: https://openagenda.com/festival-des-lumieres official: true count: 87 timespan: first: '2025-06-20T18:00:00+02:00' last: '2025-09-14T20:00:00+02:00' viewport: topLeft: { latitude: 45.808, longitude: 4.812 } bottomRight: { latitude: 45.748, longitude: 4.879 } properties: facets: type: object additionalProperties: false description: > One entry per requested facet, under the facet's name. A facet that was not requested is absent; a requested one is always present, even when no event matches — its value is then an empty array, an empty object, `null`, or its own fixed structure with zero counts, depending on the facet. Each property below defines its shape. properties: cities: type: array description: Top cities by event count. items: { $ref: '#/components/schemas/FacetBucket' } regions: type: array description: Top regions by event count. items: { $ref: '#/components/schemas/FacetBucket' } departments: type: array description: Top departments by event count. items: { $ref: '#/components/schemas/FacetBucket' } districts: type: array description: Top districts by event count. items: { $ref: '#/components/schemas/FacetBucket' } countryCodes: type: array description: Top countries (ISO 3166-1 alpha-2) by event count. items: { $ref: '#/components/schemas/FacetBucket' } keywords: type: array description: Top keywords by event count. items: { $ref: '#/components/schemas/FacetBucket' } languages: type: array description: Top languages (ISO 639-1) by event count. items: { $ref: '#/components/schemas/FacetBucket' } accessibilities: type: array description: Event count per accessibility facility code. items: { $ref: '#/components/schemas/FacetBucket' } status: type: array description: Event count per `EventStatus` code, as a string. items: { $ref: '#/components/schemas/FacetBucket' } attendanceModes: type: array description: Event count per `AttendanceMode` code, as a string. items: { $ref: '#/components/schemas/FacetBucket' } originAgendas: type: array description: Top origin agendas by event count. items: { $ref: '#/components/schemas/AgendaFacetBucket' } sourceAgendas: type: array description: Top source agendas by event count. items: { $ref: '#/components/schemas/SourceAgendaFacetBucket' } locations: type: array description: Top locations by event count. items: { $ref: '#/components/schemas/LocationFacetBucket' } geohash: type: array description: Geo clusters at the `geohashZoom` granularity. items: { $ref: '#/components/schemas/GeoFacetBucket' } viewport: description: Bounding box enclosing the matching events, or `null`. oneOf: - $ref: '#/components/schemas/Viewport' - type: 'null' timespan: description: Earliest and latest event dates, or `null`. oneOf: - $ref: '#/components/schemas/Timespan' - type: 'null' timings: type: array description: > Date histogram of occurrences, one bucket per `timingsInterval`. items: { $ref: '#/components/schemas/FacetBucket' } dateRanges: type: array description: > Occurrence counts per day over the `month` calendar month, one bucket per day, zero-count days included. items: { $ref: '#/components/schemas/FacetBucket' } additionalFields: type: object description: > One entry per requested (and readable) agenda choice/boolean field, keyed by field name. additionalProperties: x-additionalPropertiesName: fieldName allOf: - $ref: '#/components/schemas/AdditionalFieldFacet' additionalFieldMetrics: type: object description: > One entry per requested (and readable) agenda numeric field, keyed by field name. additionalProperties: x-additionalPropertiesName: fieldName allOf: - $ref: '#/components/schemas/AdditionalFieldMetricsFacet' AdditionalFieldFacet: type: object required: [label, values] additionalProperties: false description: An agenda choice/boolean field with per-option event counts. properties: label: $ref: '#/components/schemas/LocalizedString' values: type: array items: { $ref: '#/components/schemas/AdditionalFieldBucket' } AdditionalFieldBucket: type: object description: >- One option of an agenda's own field, with the number of events that carry it. required: [value, label, count] additionalProperties: false properties: value: type: string description: The option id (or `true`/`false` for boolean fields). label: description: > Localized option label, or `null` when the option has none (boolean fields). oneOf: - $ref: '#/components/schemas/LocalizedString' - type: 'null' count: type: integer description: Number of events in the filtered set with this option. AdditionalFieldMetricsFacet: type: object required: [label, metrics] additionalProperties: false description: An agenda numeric field with summary statistics. properties: label: $ref: '#/components/schemas/LocalizedString' metrics: $ref: '#/components/schemas/AdditionalFieldMetrics' AdditionalFieldMetrics: type: object required: [sum, avg, max, min] additionalProperties: false description: > Summary statistics over the field's numeric values across the filtered set. `avg`, `max` and `min` are `null` when no event in the set carries a value; `sum` is then `0`. properties: sum: type: [number, 'null'] avg: type: [number, 'null'] max: type: [number, 'null'] min: type: [number, 'null'] AgendaFacetBucket: type: object required: [agenda, count] additionalProperties: false description: > An origin-agenda facet bucket — events grouped by origin agenda. `agenda` is an `AgendaRef` without `official`. Use the uid with the `originAgendaUid` filter. properties: agenda: $ref: '#/components/schemas/AgendaRef' count: type: integer description: Number of events in the filtered set from this agenda. SourceAgendaFacetBucket: type: object required: [agenda, count] additionalProperties: false description: > A source-agenda facet bucket — events grouped by source (aggregated) agenda. Use the uid with the `sourceAgendaUid` filter. properties: agenda: $ref: '#/components/schemas/SourceAgendaRef' count: type: integer description: Number of events in the filtered set from this source agenda. LocationFacetBucket: type: object required: [location, count] additionalProperties: false description: > A location facet bucket — events grouped by their attached location. `location` carries `uid` and `name`. Use the uid with the `locationUid` filter. properties: location: type: object required: [uid, name] additionalProperties: false properties: uid: type: integer format: int64 name: type: [string, 'null'] count: type: integer description: Number of events in the filtered set at this location. GeoPoint: type: object description: A geographic point, in decimal degrees. required: [latitude, longitude] additionalProperties: false properties: latitude: type: number longitude: type: number GeoFacetBucket: type: object required: [value, count, latitude, longitude] additionalProperties: false description: > A geo-clustering bucket (from the `geohash` facet): a cluster centroid plus its event count, for plotting clustered markers on a map. Cluster granularity is controlled by `geohashZoom`. properties: value: type: string description: Opaque cluster id (the geohash grid key). count: type: integer latitude: type: number longitude: type: number Viewport: type: object required: [topLeft, bottomRight] additionalProperties: false description: > The bounding box enclosing all matching events' locations (from the `viewport` facet) — handy to auto-fit a map to the result set. The facet value is `null` when no event in the filtered set has coordinates. properties: topLeft: $ref: '#/components/schemas/GeoPoint' bottomRight: $ref: '#/components/schemas/GeoPoint' Timespan: type: object required: [first, last] additionalProperties: false description: > The date range spanned by the matching events (from the `timespan` facet): `first` is the earliest event start and `last` the latest, both RFC 3339 date-times — handy to bound a date picker to the result set. The facet value is `null` when no event in the filtered set has timings. properties: first: type: string format: date-time last: type: string format: date-time