Skip to content

Endpoint reference

Every read API endpoint, with request shape, response shape, required scopes, and example payloads.

Common conventions

Field naming

A few naming conventions you'll see across endpoints - call out in case any catch you off guard:

  • Application review state on a participant is carried by application_status alone (pending / approved / rejected / revision_requested). Reviewer identity and review timestamps are not included. On the webhook side the split is: the three pending-application events carry their state in the event type alone (application.submitted, application.rejected, application.revision_requested), because a pending application has no application_status field to carry it. application.approved is different: it carries the participant resource, which includes application_status: "approved".
  • Status field naming: status on the resource itself (e.g. application_status for applications, event.publish_status, activity.status).
  • Time fields: always ISO 8601 in UTC. There is no single suffix to key on: instants use _at (created_at, updated_at, published_at, sales_open_at), scheduled boundaries use _date or _time (start_date, end_date, start_time, end_time), and a wave's notify_when uses neither. Read the field reference for each resource rather than inferring the type from the name.
  • Enum values are served verbatim and are not guaranteed to be valid identifiers. min_age is all_ages or 18+, and that second value begins with a digit and ends with a symbol. Map enum values through a lookup rather than deriving constant names from them, and do not assume a prefixing convention.

Base URL

Environment Base
All usage https://api.revento.app/api/v1

Use this base URL for all API requests. To test your integration, connect it to an event you control through the standard OAuth flow; the base URL stays the same.

URLs in this reference are written relative to the base.

Authentication

Every request requires a Bearer header with an installation token (event-scoped) or user token (participant-scoped). Token type matters per endpoint - see the table on each endpoint page.

Authorization: Bearer rev_install_8h3k2m...
Accept: application/vnd.revento.v1+json

Accept is optional - see API versioning - but pinning it is recommended.

Request IDs

Send a correlation id on every request. Any unique-per-request value works; a v4 UUID is the recommended shape, not a requirement:

X-Revento-Request-Id: 0f8ad6e2-7e1a-4e7e-bc3c-8f5b1c4d2e3f

We echo your value back, in the X-Revento-Request-Id response header and in the request_id field of any error body, so your logs and ours join on one string.

A value we cannot use is replaced, never rejected: if the header is missing, or the value is not 8-64 characters of letters, digits, ., _ or -, the response carries an id we generated instead. You keep the response - a correlation id is a debugging aid and losing the join is not worth failing the call - but you will not see your own value, so a mismatch means the header was malformed rather than dropped.

Pagination

Every list endpoint paginates - /participants, /activities, /threads, /locations, /registration-waves, /roles and /program - and they all return the same envelope:

{
  "items":       [ /* array of resources */ ],
  "next_cursor": "opaque-string-or-null"
}

The array is items. There is no data key and no has_more field.

To fetch the next page, pass next_cursor as the cursor query parameter. The cursor is opaque; don't parse it. Stop when a page comes back with an empty items array, rather than when the cursor turns null: a last page may still carry a cursor, so a loop keyed on the cursor alone can run one iteration past the data.

limit defaults to 25, and it is clamped rather than validated: above 50 you silently get 50, below 1 you silently get 1, and a non-numeric value falls back to 25. All three answer 200, so read the length of items rather than assuming you got the page size you asked for.

A request without limit or cursor returns at most 25 rows on every one of those endpoints. An event with 52 locations answers /locations with 25 of them plus a cursor, and nothing else in the response says the rest exist. Paginate all of them, not only the ones you expect to be long.

A cursor the server cannot resolve - malformed, expired, or taken from a different collection - answers 400 validation_error with field: "cursor". It is a client-side error, so retrying the same request cannot help; start the walk again without a cursor.

Errors

Every error has the same envelope:

{
  "error":      "string_code",
  "message":    "Human-readable explanation",
  "request_id": "correlation string echoing X-Revento-Request-Id"
}

Full catalog in Errors.

Endpoint summary

Four groups, four auth styles:

Group Auth Endpoints
Event data (installation token) Authorization: Bearer rev_install_... GET /events/{id}, /participants, /participants/{participant_id}, /program, /activities, /locations, /threads, /registration-waves, /roles
User data (user token) Authorization: Bearer rev_user_... GET /me/profile
OAuth management HTTP Basic client_id:client_secret POST /oauth/revoke
Publisher tooling Authenticated publisher session POST /developer/integrations/{id}/rotate-client-secret, POST /developer/integrations/{id}/rotate-webhook-secret, POST /developer/integrations/{id}/send-test-webhook

Mixing installation and user token types returns 403 user_token_required or 403 installation_token_required. Sending an integration bearer token to publisher-tooling endpoints returns 401 invalid_token.


Event endpoints (installation token)

GET /events/{event_id}

Read event metadata.

Scope: event.read

Request

GET /api/v1/events/evt_abc123 HTTP/1.1
Host: api.revento.app
Authorization: Bearer rev_install_...
Accept: application/vnd.revento.v1+json

Response

{
  "event": {
    "id":                                 "evt_abc123",
    "created_at":                         "2026-04-01T10:30:00Z",
    "updated_at":                         "2026-05-09T14:22:00Z",
    "organization":                       "org_xyz789",
    "title":                              "Sample Convention 2026",
    "description":                        "Annual convention...",
    "start_date":                         "2026-08-15T16:00:00Z",
    "end_date":                           "2026-08-17T16:00:00Z",
    "location":                           "Main Venue, Sample City",
    "image_url":                          "https://example.com/image.jpg",
    "avatar_url":                         null,
    "access_mode":                        "open_approval",
    "min_age":                            "all_ages",
    "published_at":                       "2026-04-15T08:00:00Z",
    "sales_open_at":                      null,
    "slug":                               "sample-convention-2026",
    "format":                             "onsite",
    "event_mode":                         "standard",
    "language":                           ["pl"],
    "stream_url":                         null,
    "themes":                             ["rpg", "board_games"],
    "city":                               "Sample City",
    "country":                            "PL",
    "address":                            "1 Sample Street, 12-345 Sample City",
    "lat":                                52.2297,
    "lng":                                21.0122,
    "currency":                           "PLN",
    "is_free":                            false,
    "min_price":                          4900,
    "catalog_status":                     "listed",
    "publish_status":                     "published",
    "visibility":                         "discoverable",
    "is_archived":                        false,
    "re_entry":                           true,
    "checkout_requires_application_form": true
  }
}

The resource is wrapped in a top-level event object. All 35 fields are always present; an unavailable value is null. Webhooks use the same public event shape, documented in the event resource.

Field reference

Field Type Notes
id string Stable, opaque identifier
organization string | null The organization that owns this event, as a bare id
title string | null Display title
description string | null Free-text Markdown
start_date, end_date ISO 8601 (UTC) | null Always UTC
location string | null Free-text "headline" location. The event's individual rooms and spots are listed at /locations
image_url, avatar_url, stream_url URL | null Cover image, square avatar, live-stream link
access_mode enum | null closed | open_approval | open_auto | paid_ticket
min_age enum | null all_ages | 18+. Served verbatim; 18+ is not a safe identifier.
published_at, sales_open_at ISO 8601 (UTC) | null When the event was published, and when ticket sales open
slug string | null URL slug
format, event_mode string | null Event format and mode
language string[] | null An array of ISO-639-1 codes, not a single string
themes string[] | null Theme keys
city, country, address string | null Postal location
lat, lng float | null Coordinates
currency, is_free, min_price string | bool | int | null Pricing summary; min_price is in minor units
catalog_status string | null Public catalog listing state
publish_status enum | null draft | published
visibility enum | null private | discoverable
is_archived, re_entry, checkout_requires_application_form bool | null Operational flags

GET /events/{event_id}/participants

List participants of the event. Returns the event-scoped profile (not the global Revento user profile) - including custom form responses and PII fields where present.

Scope: participants.read

Request

GET /api/v1/events/evt_abc123/participants?limit=50 HTTP/1.1
Authorization: Bearer rev_install_...

Response

{
  "items": [
    {
      "id":                      "ptc_9f2a7c1e4b...",
      "created_at":              "2026-04-29T14:00:00Z",
      "updated_at":              "2026-05-02T09:14:00Z",
      "display_name":            "Sample Name",
      "nickname":                "Sample",
      "organisation":            "Sample Org",
      "privacy_mode":            false,
      "is_guest":                false,
      "role":                    "role_attendee",
      "real_name":               "Sample Real Name",
      "email":                   "sample@example.com",
      "admin_notes":             null,
      "application_status":      "approved",
      "application_admin_notes": null,
      "custom_form_fields":      {
        "favorite_track":  "RPG",
        "dietary_notes":   "Vegetarian",
        "arrival_day":     null
      },
      "personal_data_withheld":  false
    }
  ],
  "next_cursor": "eyJjIjogIjIw..."
}

id is a per-integration pseudonym

id is a per-integration pseudonym, and this resource carries no user_id field. The same person carries a different id for you than for any other integration, and a stable one across events for you.

This is the identifier GET /events/{event_id}/participants/{participant_id} accepts back, and the identical value the participant's webhook payloads carry, so the two channels join without exposing a shared identifier.

Joining a signed-in participant to their participant record. The user_id from the OAuth token response is already this value, so you can match it against id here without an extra call. GET /me/profile returns the same string as its user_id, and so does the sub claim of the id_token.

Roles

role is the role id as a bare string, not a nested object. Roles are event-scoped - every event defines its own (typically Attendee, Organizer, Staff, NPC, but custom roles are possible). Resolve ids to names once via GET /events/{id}/roles and cache them.

Application statuses

pending | approved | rejected | revision_requested

admin_notes vs application_admin_notes

Two different organizer-authored notes. admin_notes is the organizer's private note on the person; application_admin_notes is their note on that person's application. They are not interchangeable.

custom_form_fields

A nested JSON object whose keys are the custom participant fields the organizer declared for this event, so they are event-specific. Every declared field is present, null when the participant did not answer it, so you can rely on the key existing. Undeclared fields do not appear. Keys are organizer-chosen, so treat the key set as event-specific data rather than a fixed schema.

personal_data_withheld

Always present. false means the participant's available data was included, so a null field means no value is available. true means personal data was withheld: every personal field is null and custom_form_fields is null as a whole. The same field has the same meaning in webhook payloads.

Webhooks use the same public participant shape; see the participant resource.


GET /events/{event_id}/participants/{participant_id}

Read one participant by id.

Scope: participants.read

Request

GET /api/v1/events/evt_abc123/participants/ptp_001 HTTP/1.1
Authorization: Bearer rev_install_...
Accept: application/vnd.revento.v1+json

Response

{
  "participant": {
    "id":                      "ptc_9f2a7c1e4b...",
    "created_at":              "2026-04-29T14:00:00Z",
    "updated_at":              "2026-05-02T09:14:00Z",
    "display_name":            "Sample Name",
    "nickname":                "Sample",
    "organisation":            "Sample Org",
    "privacy_mode":            false,
    "is_guest":                false,
    "role":                    "role_attendee",
    "real_name":               "Sample Real Name",
    "email":                   "sample@example.com",
    "admin_notes":             null,
    "application_status":      "approved",
    "application_admin_notes": null,
    "custom_form_fields":      {
      "favorite_track":  "RPG",
      "dietary_notes":   "Vegetarian",
      "arrival_day":     null
    },
    "personal_data_withheld":  false
  }
}

The resource is wrapped in a top-level participant object. Its shape matches an item from GET /events/{event_id}/participants.


GET /events/{event_id}/program

Read the event's program - threads with nested activities. Activities have start_time and end_time (UTC) - group them by date on your side if you need a day-level view.

Scope: event.read

Response

{
  "items": [
    {
      "id":          "thr_main",
      "created_at":  "2026-04-10T09:00:00Z",
      "updated_at":  "2026-04-10T09:00:00Z",
      "name":        "Main Hall",
      "description": "All headline activities",
      "icon_name":   "auditorium",
      "color_hex":   "#2ED75A",
      "sort_order":  0,
      "activities": [
        { /* full activity object - see GET /events/{event_id}/activities */ }
      ]
    }
  ],
  "next_cursor": "eyJjIjogInRoci4u..."
}

Each thread carries the same eight fields /threads serves, plus the activities array this endpoint adds.

For paginated flat access to activities, use /activities.


GET /events/{event_id}/activities

List activities, paginated. Use this when you don't need the program tree structure or when there are many activities.

Scope: event.read

Response

{
  "items": [
    {
      "id":                      "act_001",
      "created_at":              "2026-04-20T10:00:00Z",
      "updated_at":              "2026-08-15T14:30:00Z",
      "title":                   "Opening Ceremony",
      "description":             "Welcome and overview of the weekend",
      "activity_type":           "rpg_session",
      "activity_details":        "adt_001",
      "type_definition":         "atd_rpg",
      "status":                  "scheduled",
      "start_time":              "2026-08-15T16:00:00Z",
      "end_time":                "2026-08-15T17:00:00Z",
      "time_preference":         [],
      "is_global":               false,
      "capacity":                8,
      "sign_ups_enabled":        true,
      "committed_signups_count": 5,
      "players":                 ["ptc_a1b2...", "ptc_c3d4...", "ptc_e5f6..."],
      "host_id":                 "ptc_9f2a...",
      "host":                    "ptc_9f2a...",
      "host_display_name":       "Sample Host",
      "co_host_ids":             ["ptc_7g8h...", "ptc_9i0j..."],
      "co_host_display_names":   "Co-host A, Co-host B",
      "markers":                 ["mkr_kids"],
      "registration_waves":      ["wave_a"],
      "thread":                  "thr_main",
      "location":                "loc_main_hall",
      "personal_data_withheld":  false
    }
  ],
  "next_cursor": null
}

Webhooks use the same public activity shape; see the activity resource.

Field reference

Field Type Notes
activity_type enum rpg_session | lecture | workshop | competition | discussion | LARP | other | break
activity_details, type_definition, thread, location string | null Bare ids, not nested objects. Resolve and cache them separately - the activity carries the reference, not a copy of the other resource.
start_time, end_time ISO 8601 (UTC) | null null until the activity is scheduled
status enum new | scheduled | canceled
host_id, host, co_host_ids, players string | null, string[] | null Per-integration pseudonyms, the same values the participant resource carries as its id. host_id and host name the same person. Display names are provided in host_display_name and co_host_display_names (comma-joined).
players string[] | null The participants currently signed up. Diff it against your stored copy to detect individual sign-ups and sign-outs.
committed_signups_count int How many have actually committed
markers string[] | null Bare ids of tag-like badges (e.g. "Kids-friendly", "Advanced")
registration_waves string[] | null IDs of waves that gate sign-ups for this activity
personal_data_withheld bool Always present. false means every person involved in this activity is named, so an empty host or sign-up field means the activity genuinely has nobody in that role. true means at least one of them was withheld, so an empty value is a redaction rather than a vacancy. Same field, same meaning, on the activity webhook payloads.

GET /events/{event_id}/locations

The event's locations - the rooms and spots activities point at. A flat, paginated list of location rows.

Scope: event.read

Response

{
  "items": [
    {
      "id":              "loc_main_hall",
      "created_at":      "2026-04-10T09:00:00Z",
      "updated_at":      "2026-04-10T09:00:00Z",
      "name":            "Main Hall",
      "code":            "MH-1",
      "description":     "...",
      "scope":           "indoor",
      "type":            "room",
      "is_schedulable":  true,
      "capacity":        150,
      "amenities":       ["projector", "wifi"],
      "position_x":      50.0,
      "position_y":      50.0,
      "icon_override":   null,
      "color_hex":       null,
      "operating_hours": "08:00-22:00",
      "sort_order":      0,
      "venue":           "ven_default",
      "building":        "bld_main",
      "floor":           "flr_0",
      "thread":          "thr_main"
    }
  ],
  "next_cursor": "eyJjIjogImxvYy4u..."
}

There is no venue graph on this endpoint. It does not return venues with nested buildings and floors. It returns location rows, and venue, building, floor and thread on each row are bare ids or null, matching the location webhook shape. Group by venue and building on your side if you need a tree.

All 21 fields are always present, null when a value is unavailable.

This list paginates, default page 25. An event with 52 locations answers the first request with 25 rows plus a cursor; keep requesting with cursor until a page comes back with an empty items array.

Location types: room | stage | expo | food | restroom | entrance | info | parking | rest_area | first_aid | smoking_area | other

Scopes: indoor | outdoor


GET /events/{event_id}/threads

The flat list of program threads, with display metadata. Activities reference threads by id; use this if you need to render thread color/icon outside the program tree.

Scope: program.read

Response

{
  "items": [
    {
      "id":          "thr_main",
      "created_at":  "2026-04-10T09:00:00Z",
      "updated_at":  "2026-04-10T09:00:00Z",
      "name":        "Main Hall",
      "description": "All headline activities",
      "icon_name":   "auditorium",
      "color_hex":   "#2ED75A",
      "sort_order":  0
    }
  ],
  "next_cursor": null
}

All eight fields are always present. description, icon_name and color_hex are nullable: a thread the organizer created without an icon or colour carries null, not a default.


GET /events/{event_id}/registration-waves

List the event's registration waves - the time windows during which activity sign-ups are open.

Scope: program.read

Response

{
  "items": [
    {
      "id":                            "wave_a",
      "created_at":                    "2026-04-10T09:00:00Z",
      "updated_at":                    "2026-05-10T08:00:00Z",
      "name":                          "Friday morning",
      "start_time":                    "2026-05-10T08:00:00Z",
      "end_time":                      "2026-05-10T12:00:00Z",
      "status":                        "scheduled",
      "capacity":                      100,
      "capacity_mode":                 "fixed",
      "per_user_limit":                3,
      "base_activities":               ["act_001", "act_002", "act_003"],
      "roles_ids":                     ["role_attendee"],
      "audience":                      "all",
      "notify_when":                   "2026-05-09T18:00:00Z",
      "active_eligibility_generation": 1
    }
  ],
  "next_cursor": null
}

All 15 fields are always present, null when the wave holds no value. This is the same wave object the webhooks deliver - see the registration-wave resource.

Field reference

Field Type Notes
name string | null Display name
start_time, end_time ISO 8601 (UTC) | null The wave's window. Not starts_at / ends_at.
status enum | null scheduled | started | finished | canceled
capacity int | null Places the wave offers
capacity_mode enum | null fixed | percentage
per_user_limit int | null Sign-ups one participant may hold in this wave
base_activities string[] | null Bare ids of the activities this wave gates. Not activity_ids.
roles_ids string[] | null Bare ids of the roles the wave is open to
audience enum | null all | roles
notify_when ISO 8601 (UTC) | null A timestamp, not a trigger name: when participants are notified about this wave
active_eligibility_generation int | null Opaque eligibility revision value; do not interpret or increment it

GET /events/{event_id}/roles

The event's role catalog. Participants reference roles by id; use this to resolve role names if you cache participants.

Scope: event.read

Response

{
  "items": [
    {
      "id":          "role_attendee",
      "created_at":  "2026-04-01T10:30:00Z",
      "updated_at":  "2026-04-01T10:30:00Z",
      "name":        "Attendee",
      "description": null
    },
    {
      "id":          "role_organizer",
      "created_at":  "2026-04-01T10:30:00Z",
      "updated_at":  "2026-04-01T10:30:00Z",
      "name":        "Organizer",
      "description": "Runs the event"
    }
  ],
  "next_cursor": null
}

All five fields are always present; description is null when the organizer wrote none.


User endpoints (user token)

GET /me/profile

Read the signed-in participant's own profile, plus their event-scoped participant record.

Scope: profile.read

Response

{
  "profile": {
    "user_id": "ptc_9f2a7c1e4b...",
    "name":    "Sample Name",
    "email":   "sample@example.com",
    "event_profile": { /* the participant object - see GET /events/{event_id}/participants */ }
  }
}

Everything is nested under a top-level profile object.

Field reference

Field Type Notes
user_id string The per-integration pseudonym. It is the same value as event_profile.id and as the participant's id on the installation-token endpoints. See below.
name string The participant's account name. Absent if the account cannot be read.
email string The participant's account email. Absent if the account cannot be read.
event_profile object The participant's own record on the event this token is bound to. It uses the same participant shape, field for field, as GET /events/{event_id}/participants. Absent when the participant has no event profile.

user_id is the pseudonym, and it is the join key

user_id here is the per-integration pseudonym. It is equal to event_profile.id, equal to the participant's id on the installation-token endpoints, equal to the id in their webhook payloads, and it is accepted back by GET /events/{event_id}/participants/{participant_id}. One person is one identifier for you across every surface.

It is the same string the OAuth token response returns as user_id, and the same as the id_token sub claim. If you already hold a token response you do not need to call this endpoint just to obtain the join key.

If the platform cannot issue a pseudonym, the field is omitted from the response rather than falling back to the account identifier. Treat an absent user_id as "no join key available", not as "no user".

name and email are included if the participant granted profile.read. The scope is per-participant and may be declined on the consent screen.

There is no locale field, and no event_id field. The event is the one your user token is bound to, and the token response carries its id.

Email verification state is not part of the v1 contract. If an email is present, treat it as user-provided profile data, not as proof that the address was verified.

event.attendance does not unlock a user-token endpoint

event.attendance discloses only attendance confirmation - no application data, no email, no roles. The existence of an issued user token IS the confirmation. There is no /me/application endpoint in v1. Integrations that need participant application state (status, form responses, review action) read it from the installation-token endpoint GET /events/{id}/participants (scope: participants.read) and match rows by the pseudonym GET /me/profile returns as user_id. Combining the two token types is the v1 design.


Developer and OAuth management endpoints

Two auth models exist here:

  • POST /oauth/revoke uses your integration's client_id + client_secret (HTTP Basic auth).
  • The /developer/integrations/... endpoints use your authenticated publisher session in Revento's integration-management tools. They are not integration bearer-token endpoints.

POST /oauth/revoke

Revoke a token (RFC 7009).

Request

POST /oauth/revoke HTTP/1.1
Host: auth.revento.app
Content-Type: application/x-www-form-urlencoded
Authorization: Basic {base64(client_id:client_secret)}

token={the access or refresh token to revoke}
&token_type_hint=access_token

token_type_hint is optional; values are access_token or refresh_token.

Response

Always 200 OK with empty body, including for unknown tokens (prevents information disclosure about token validity).

Revoking an access token invalidates only that token. Revoking a refresh token invalidates the entire token family.

POST /developer/integrations/{integration_id}/rotate-client-secret

Generate a new OAuth client_secret for your integration. The current and previous client secret remain valid at /oauth/token for a 24-hour overlap window so you can roll the new secret through your fleet without downtime.

Auth: authenticated publisher session

Request: empty body.

Response

{
  "client_secret":              "rev_client_secret_new_xyz789...",
  "previous_secret_expires_at": "2026-05-10T14:30:00Z"
}

The new secret is shown once in this response. Store it securely before leaving the page or closing the flow that initiated the rotation.

Operational notes:

  • Rate-limited to 3 rotations per 24 hours per integration.
  • The management flow may require a fresh re-authentication step before completing the rotation.

POST /developer/integrations/{integration_id}/rotate-webhook-secret

Generate a new HMAC signing secret for your integration's webhook deliveries.

Auth: authenticated publisher session

Request: empty body.

Response

{
  "secret":                     "whsec_newSecret_xyz789...",
  "previous_secret_expires_at": "2026-05-10T14:30:00Z"
}

The new secret is shown once in this response. Store it before navigating away.

During the 24-hour overlap, deliveries carry:

X-Revento-Signature:           sha256={signature with new secret}
X-Revento-Signature-Previous:  sha256={signature with previous secret}

Note on the subscription model: your integration has a single webhook URL and a single signing secret, declared in your integration manifest at registration time. There are no per-event-type subscription objects to manage - when your manifest declares an event type, you receive deliveries for it automatically as soon as you have an installation token that's authorized for the matching event.

If you need to change which event types you receive: update your integration manifest. If you need to change your webhook URL: update your manifest. Manifest edits don't trigger re-consent unless they add scopes.

Operational notes:

  • Rate-limited to 3 rotations per 24 hours per integration.

POST /developer/integrations/{integration_id}/send-test-webhook

Send one representative signed webhook to your configured webhook_url without waiting for a real event.

Auth: authenticated publisher session

Request

{
  "event_type": "application.approved"
}

event_type must be one of the event types already declared in your manifest.

Response

{
  "status_code":            200,
  "response_time_ms":       184,
  "response_body_preview":  "{\"ok\":true}",
  "signature_header_value": "sha256=8e1c4b..."
}

Operational notes:

  • Rate-limited to 10 test deliveries per hour per integration.
  • Uses the same signature format and headers as normal webhook delivery.
  • Truncates response_body_preview to 1 KB.
  • It is sent once, without retries, and does not appear in replay history.

API versioning

Version Status
v1 Current

Accept is optional. Omitting it serves the current version, and so do */* and application/*. Pin Accept: application/vnd.revento.v1+json in production anyway, so a future version transition cannot silently change your response shape.

A concrete media type this API does not produce is refused with 406 not_acceptable. application/json on its own is refused, as is a version that does not exist such as application/vnd.revento.v9+json. The choice is between saying nothing and naming something we serve; naming something we do not is the only failing case.

Request and response media types differ here, and the spec models the response. Every response carries Content-Type: application/json, so that is what openapi.yaml declares as the response representation; a generated decoder therefore sees the type it will actually receive.

The versioned type is a request requirement, not a response representation, and the spec expresses it through the 406 response rather than through the response content map. If your generator derives Accept from response media types it will send application/json and take a 406. Override it to send application/vnd.revento.v1+json, or to send no Accept header at all.

Do not gate deserialization on the response Content-Type matching the Accept you sent; they are not the same value.

Any future version is supported alongside the previous one for at least 6 months after announcement, with the deprecation surfaced both in the Changelog and in Revento's integration-management notices.