Skip to content

Webhook event catalog

Every webhook event type, with payload schema, when it fires, and deduplication semantics.

Event count and categories

V1 publishes 28 webhook event types across these categories:

Category Event types
Event lifecycle 4
Application lifecycle 4
Participant lifecycle 3
Activity lifecycle 4
Thread lifecycle 3
Location lifecycle 3
Registration-wave lifecycle 3
Data deletion 1
User consent 1
Integration lifecycle 2

For machine-readable schema names for all 28 events, see Schemas & tooling.

Common envelope

Every webhook delivery has the same headers:

POST {your webhook URL} HTTP/1.1
Content-Type:           application/json
X-Revento-Event:        {event type, e.g. "application.approved"}
X-Revento-Delivery-Id:  {delivery id - unique per delivery, retries reuse this}
X-Revento-Timestamp:    {unix epoch seconds}
X-Revento-Signature:    sha256={HMAC-SHA256 of timestamp + "." + body}
X-Revento-Signature-Previous: {same format, only during 24h secret-rotation overlap}

Body shape for the 25 resource events:

{
  "event_type":      "application.approved",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-09T14:22:31Z",
  "data":            { /* event-specific resource snapshot */ }
}

The discriminator field is event_type, and it matches the X-Revento-Event header. There is no top-level id field: the delivery identifier is carried only in the X-Revento-Delivery-Id header.

The three operator lifecycle notices are flat: integration.suspended, integration.unpublished and data.deletion_required carry their fields directly on the envelope and have no data object at all. They describe a state change of your integration, not a resource, so there is no subject to put under data. Each is documented individually below.

The data object carries the resource snapshot at the moment the event occurred. You can act on it directly without making an API call to fetch the resource.

Every declared field is always present

A resource data object always carries every field its schema declares. An unavailable value is null; the field is never omitted.

This guarantees a consistent shape: you can write one deserializer per resource type and rely on every documented key being present.

The same rule applies to the nested custom_form_fields object on participant payloads: every field the organizer declared for that event is present, null when the participant did not answer it.

Identifiers are bare strings, never nested objects

Where a resource references another resource, the payload carries the referenced id as a plain string, not an object or a copy of the other resource's fields.

"venue":    "ven_default",
"building": "bld_main",
"thread":   "thr_main"

Read the referenced entity once through the API and cache it; names of venues, buildings, floors and threads change rarely.

This is the same shape the read API serves, so GET /events/{event_id}/activities and activity.updated deserialize with the same code.

Participant identifiers are per-integration pseudonyms

Every field that identifies a participant carries a pseudonym derived per integration: the same person has a different identifier for you than for any other integration, and a stable one across events for you.

actor_id is not a participant identifier. On lifecycle notices it is an opaque audit reference: it is not resolvable through this API and must not be joined against participant records.

The pseudonym is what GET /events/{event_id}/participants/{participant_id} accepts back, so you can resolve it against the API. Fields carrying pseudonyms: the participant resource's id, and an activity's host_id, host, players and co_host_ids.

Deduplication

X-Revento-Delivery-Id is the transport-level deduplication key. Its reuse differs by delivery path:

  • Automatic retries reuse the delivery id. A delivery you processed but failed to acknowledge in time is retried with the same id. Detect it and acknowledge idempotently rather than processing twice.
  • A repeated announcement reuses the delivery id. If the platform announces a change it already announced, the id is the one you already saw, so it collapses against your existing record.
  • A dead-letter replay gets a NEW delivery id. A replay carries the same fact with a fresh id, so a handler keyed only on X-Revento-Delivery-Id will process it a second time.

The practical rule: deduplicate transport-level repeats on X-Revento-Delivery-Id, but make the effect of handling a fact idempotent at the fact level too, keyed on the resource id plus whatever the per-event "dedup key" below names. The two layers cover different failures, and the dead-letter replay is the case that defeats the first one on its own.

Snapshot freshness

The data snapshot is the source of truth for the event itself. application.approved carries the application as approved, and that is not going to change retroactively. Process the snapshot directly.

If your handler delays processing for hours and you specifically need the current state of the resource at processing time, call the API with the resource id. This is uncommon; most handlers should use the snapshot.


Event types

The event resource

Four event types carry the same event resource under data: event.published, event.unpublished, event.visibility_changed and event.access_mode_changed. They differ only in event_type and in which field the organizer just changed.

{
  "event_type":      "event.published",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-04-15T08:00:00Z",
  "data": {
    "id":               "evt_abc123",
    "created_at":       "2026-04-01T10:30:00Z",
    "updated_at":       "2026-04-15T08:00: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
  }
}

organization is the organization id as a bare string.

event.published

The event transitioned from draft to published.

Fires when: an organizer publishes a previously-draft event.

Dedup key: id.

event.unpublished

The event transitioned from published back to draft.

Payload: the event resource above, with publish_status: "draft".

Dedup key: (id, updated_at).

event.visibility_changed

The event's visibility changed (between private and discoverable).

Payload: the event resource above, carrying the new visibility. The payload does not carry the previous value; diff against your stored copy if you need it.

Dedup key: (id, updated_at).

event.access_mode_changed

The event's access_mode changed (between closed, open_approval, open_auto and paid_ticket).

Payload: the event resource above, carrying the new access_mode. As with visibility, the previous value is not included.

Dedup key: (id, updated_at).


The participant resource

participant.registered, participant.profile_updated, participant.removed and application.approved all carry the participant resource.

The participant resource combines the participant's event profile and application data, including every custom field the organizer defined for that event.

{
  "event_type":      "participant.registered",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-03T10:25:00Z",
  "data": {
    "id":                      "ptc_9f2a7c1e4b...",
    "created_at":              "2026-04-29T14:00:00Z",
    "updated_at":              "2026-05-03T10:25: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":             "Seat near the stage",
    "application_status":      "approved",
    "application_admin_notes": null,
    "custom_form_fields": {
      "favorite_track":  "RPG",
      "dietary_notes":   "Vegetarian",
      "arrival_day":     null
    },
    "personal_data_withheld":  false
  }
}

id is the per-integration pseudonym. There is no user_id field.

role is the role id as a bare string.

admin_notes vs application_admin_notes. Two different notes exist and they are not interchangeable. admin_notes is the organizer's private note on the person; application_admin_notes is the organizer's note on their application. Both are organizer-authored free text.

custom_form_fields is a nested object, never spread onto the top level. Its keys are the field keys the organizer defined for this event, so they are event-specific and arbitrary. Nesting them keeps an organizer-chosen key such as email from colliding with the contract field of the same name. Every field the organizer declared is present; a field the participant did not answer is null.

The set of exported custom fields is exactly the set the organizer declared. Undeclared fields do not appear here.

personal_data_withheld

Present on every payload that describes a person, participant and application resources alike. It is a boolean and it is always present, including when it is false.

value meaning
false The platform served this person's data in full. Fields that are null are null because the platform holds no value for them.
true The platform holds personal data for this person and withheld it. Every personal field on the payload is null, and custom_form_fields is null as a whole. Non-personal fields (application_status, role, is_guest, timestamps) are still served.

The distinction matters because null alone cannot express it. On false, a null real_name means "we have nothing" and you can safely clear your local copy. On true, it means "we have it and are not giving it to you", so leave your local copy alone.

The activity resource carries a field of the same name and the same meaning, "personal data was withheld from this payload", scoped to the people that resource describes.

participant.registered

An applicant has been confirmed as a participant on the event.

Fires when: the participant transitions to registered status, either after their application is approved or immediately on submission for events with access_mode: open_auto.

Dedup key: id.

participant.profile_updated

A participant's event-scoped profile changed, either because the participant edited it themselves or because an organizer edited it on their behalf.

Payload: the participant resource above, with the post-update state. The payload does not identify who made the change.

Dedup key: (id, updated_at).

participant.removed

A participant was removed from the event.

Fires when: an organizer removes the participant from the event.

Removal revokes access; it does not erase the participant record. What goes is event membership and registration-wave eligibility. The event-scoped profile and application data remain available through the participant endpoint.

Two consequences worth handling:

  • GET /events/{event_id}/participants/{participant_id} still returns them after this event arrives. The webhook is the authoritative signal that they are no longer a participant; a successful read afterwards is not a contradiction of it.
  • They cannot simply re-apply. A further application from the same person is refused while their existing application record stands, unless that application had been rejected or sent back for revision. Treat participant.removed as terminal for that person on that event rather than waiting for a later participant.registered to pair with it.

Payload: the participant resource as it was before removal, so you can identify who left.

Dedup key: id.


The application resource

application.submitted, application.rejected and application.revision_requested describe someone who has applied to an event but is not yet a participant. They carry a thinner resource than a participant, because a pending application holds less:

{
  "event_type":      "application.submitted",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-04-29T14:00:00Z",
  "data": {
    "id":                     "ptc_9f2a7c1e4b...",
    "created_at":             "2026-04-29T14:00:00Z",
    "updated_at":             "2026-04-29T14:00:00Z",
    "nickname":               "Sample",
    "organisation":           "Sample Org",
    "privacy_mode":           false,
    "real_name":              "Sample Real Name",
    "email":                  "sample@example.com",
    "personal_data_withheld": false
  }
}

There is no display_name, is_guest or role on this resource. A pending application has no such values, and the platform does not synthesize them.

id is the same pseudonym the person will carry as a participant. That is what lets you join an application to the registration it becomes: application.submitted and the later participant.registered for the same person carry the identical id.

The application's state is the event type itself: application.submitted vs .rejected vs .revision_requested. There is no status field on this resource, and no reviewer identity, review message or status history.

application.submitted

A participant submitted an application to the event.

Fires when: the participant submits the application form. Re-submission after a revision request fires application.submitted again.

Dedup key: (id, updated_at).

application.approved

An organizer approved the application.

Payload: this one carries the participant resource, not the application resource. On approval the person becomes a participant, and the payload describes them as one. See the participant resource above.

Dedup key: id.

application.rejected

An organizer rejected the application.

Payload: the application resource above, as it was before rejection.

Dedup key: (id, updated_at).

application.revision_requested

An organizer asked the applicant to revise their application before reviewing again.

Payload: the application resource above. The revision message the organizer wrote is not included.

Dedup key: (id, updated_at).


The activity resource

activity.created, activity.updated, activity.deleted and activity.capacity_state_changed all carry the same activity resource.

{
  "event_type":      "activity.created",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-04-20T10:00:00Z",
  "data": {
    "id":                      "act_001",
    "created_at":              "2026-04-20T10:00:00Z",
    "updated_at":              "2026-04-20T10:00:00Z",
    "title":                   "Opening Ceremony",
    "description":             "Welcome and overview",
    "activity_type":           "rpg_session",
    "activity_details":        "adt_001",
    "type_definition":         "atd_rpg",
    "status":                  "new",
    "start_time":              null,
    "end_time":                null,
    "time_preference":         [],
    "is_global":               false,
    "capacity":                8,
    "sign_ups_enabled":        false,
    "committed_signups_count": 0,
    "players":                 [],
    "host_id":                 null,
    "host":                    null,
    "host_display_name":       null,
    "co_host_ids":             [],
    "co_host_display_names":   null,
    "markers":                 [],
    "registration_waves":      ["wave_a"],
    "thread":                  "thr_main",
    "location":                null,
    "personal_data_withheld":  false
  }
}

activity_details, type_definition, thread and location are bare ids, not nested objects.

players, host_id, host and co_host_ids carry per-integration pseudonyms, the same ones the participant resource uses as its id.

personal_data_withheld on an activity

Present on every activity payload, always a boolean, never omitted. It describes the people fields of this activity: host_id, host, host_display_name, co_host_ids, co_host_display_names and players.

value meaning
false Every person this activity involves is named. A people field that is null or [] is empty because the activity genuinely has nobody in that role: no host assigned, no co-hosts, nobody signed up.
true At least one person involved in this activity was withheld from the payload. Their pseudonym and display name are null (or missing from the list), and the platform is not telling you who they are.

This is the field that makes host_id: null readable. Without it that one value means two different things, "this activity has no host" and "this activity has a host you are not being given", and nothing else in the payload separates them. The same ambiguity applies to co_host_ids and players arriving short.

What to do with each: on false, an empty people field is safe to mirror: clear your local host, show "no host assigned". On true, leave what you already hold alone and do not render "no host": you are looking at a redaction, not a vacancy.

Unlike the participant resource's flag of the same name, this one does come back true in normal operation. The platform withholds people from an activity for reasons that have nothing to do with your integration, so treat true as an ordinary state to handle rather than an exception.

The flag is per payload, not per person: it tells you that something was withheld from this activity, not which role or how many.

activity.created

A new activity was added to the event's program.

Dedup key: id.

activity.updated

An existing activity changed: any field-level edit.

Dedup key: (id, updated_at). Rapid successive edits produce multiple deliveries; apply the latest snapshot by updated_at.

activity.deleted

An activity was removed from the program.

Payload: the full activity resource as it was before deletion, not a tombstone. You receive the activity you are being told to remove.

Dedup key: id. An activity can only be deleted once.

activity.capacity_state_changed

The signup state for an activity changed: a participant signed up or signed out, or capacity changed.

Payload: the full activity resource, with the current capacity, committed_signups_count, sign_ups_enabled and players. Diff players against your local state to detect individual sign-ups and sign-outs.

Dedup key: (id, updated_at).


The thread resource

thread.created, thread.updated and thread.deleted carry the same resource. A thread is a parallel track within the event, e.g. "RPG Sessions" or "Lectures".

{
  "event_type":      "thread.created",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-04-10T09:00:00Z",
  "data": {
    "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
  }
}

thread.created

A new thread was added to the event.

Dedup key: id.

thread.updated

An existing thread changed: any field-level edit.

Dedup key: (id, updated_at).

thread.deleted

A thread was removed from the event.

Payload: the same full resource as it was before deletion, not a tombstone.

Dedup key: id. A thread can only be deleted once.


The location resource

location.created, location.updated and location.deleted carry the same resource.

{
  "event_type":      "location.created",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-04-10T09:00:00Z",
  "data": {
    "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"
  }
}

venue, building, floor and thread are bare ids. The venue's address, the building's name and the floor's number are not included; fetch and cache those separately.

location.created

A new location was added to the event's venue map.

Dedup key: id.

location.updated

An existing location changed: any field-level edit, including a move between venue, building or floor.

Dedup key: (id, updated_at).

location.deleted

A location was removed.

Payload: the same full resource as it was before deletion.

Dedup key: id. A location can only be deleted once.


The registration-wave resource

registration_wave.started, registration_wave.finished and registration_wave.canceled carry the same resource. One deserializer covers all three; the event type and the resource's own status tell you which ending you got.

{
  "event_type":      "registration_wave.started",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-10T08:00:00Z",
  "data": {
    "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":                        "started",
    "capacity":                      100,
    "capacity_mode":                 "fixed",
    "per_user_limit":                3,
    "base_activities":               ["act_001", "act_002"],
    "roles_ids":                     ["role_attendee"],
    "audience":                      "all",
    "notify_when":                   "2026-05-09T18:00:00Z",
    "active_eligibility_generation": 1
  }
}

The wave's timestamps are start_time / end_time. base_activities and roles_ids are arrays of bare ids.

registration_wave.started

The wave transitioned to started, so sign-ups for activities gated by it are now open.

Dedup key: (id, updated_at).

registration_wave.finished

The wave ran to the end of its window and transitioned to finished, so sign-ups gated by it are now closed.

Payload: the same resource with status: "finished".

Dedup key: (id, updated_at).

registration_wave.canceled

An organizer stopped the wave by hand before it ran its course. Sign-ups gated by it are closed, exactly as with finished.

Fires when: an organizer stops a wave from the event's registration-wave controls.

Payload: the same resource with status: "canceled".

Dedup key: (id, updated_at).

Branch on this separately from finished. The two are not variants of one another and the platform will never send finished for a wave that was stopped:

registration_wave.finished registration_wave.canceled
what happened the wave reached the end of its window an organizer stopped it early
status in the payload finished canceled
does finished arrive for this wave yes, this is that event no, never

End-of-wave logic (settlement, reporting, releasing held places, "the wave has closed" notifications) is written against finished and should stay there. A canceled wave never ran its course, so applying that logic to it acts on a conclusion the wave never reached. If all you need is "sign-ups are shut", handle both.

Without this event a stopped wave would stay open forever in your model: the wave closes on our side, no further event is produced for it, and nothing in the data distinguishes that from a wave still in progress.


user.revoked_access

A specific participant has revoked their "Login with Revento" consent for your integration.

Fires when: the participant revokes access to your integration in Revento.

{
  "event_type":      "user.revoked_access",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-09T16:00:00Z",
  "data": {
    "participant_id": "ptc_9f2a7c1e4b...",
    "granted_scopes": ["profile.read", "event.attendance"],
    "revoked_at":     "2026-05-09T16:00:00Z"
  }
}

The person is identified by participant_id, the same per-integration pseudonym their participant resource carries. There is no user_id field. integration_id and event_id are on the envelope, not repeated under data.

revoked_at is this fact's own timestamp; occurred_at is the transport's. They are the same instant today because the notice is produced synchronously with the revocation.

After receiving this, delete any per-user data for that participant_id in the context of event_id. Other events the same user is signed in to are unaffected; each (user, event) pair is independent.

Dedup key: (participant_id, event_id, revoked_at).


Operator lifecycle notices

The remaining three types are flat: their fields sit directly on the envelope and there is no data object.

data.deletion_required

The integration's access to this event's data has ended. You are contractually expected to delete your cached copy.

Fires when: an event is archived, the organization loses formal status, the integration publisher unpublishes, or an operator force-deletes the integration.

{
  "event_type":      "data.deletion_required",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-09T14:30:00Z",
  "actor_id":        "usr_org_123",
  "trigger":         "event_archived",
  "scope":           "event",
  "root_id":         "evt_abc123",
  "deletion_mode":   "soft_pending",
  "reason":          "Organizer archived the event"
}

All four of trigger, scope, root_id and deletion_mode are always present.

field values
trigger event_archived, organization_verification_rejected, integration_unpublished, operator_force_delete
scope event, organization, integration
deletion_mode soft_pending, immediate

root_id is the entity the deletion started from, interpreted according to scope.

deletion_mode is the field to branch on. With soft_pending the notice is queued immediately but held for a grace window before dispatch, so a cascade an operator reverses inside that window never reaches you at all; by the time you receive it, the deletion is settled. With immediate an operator force-deleted the integration: there is no window, nothing to cancel, and the first three trigger values cannot occur alongside it (operator_force_delete is the only trigger that pairs with immediate).

reason is optional free text and is absent when the operator recorded none. It is the one field on this payload that may be missing rather than null.

Dedup key: (integration_id, event_id, occurred_at).

Contract note: there is no acknowledgment endpoint. The dispatch audit entry records that you were informed.

integration.unpublished

You, the integration publisher, have unpublished the integration. One of these fires per affected (event, organization) tuple, alongside data.deletion_required.

{
  "event_type":      "integration.unpublished",
  "integration_id":  "int_yourapp",
  "organization_id": "org_xyz789",
  "event_id":        "evt_abc123",
  "occurred_at":     "2026-05-09T19:00:00Z",
  "actor_id":        "usr_pub_123",
  "reason":          "Retiring v1 of the app"
}

The affected event and organization are the envelope's event_id and organization_id. They are not repeated under other names. reason is optional and absent when none was recorded.

Dedup key: (integration_id, event_id, organization_id, occurred_at).

integration.suspended

Revento has suspended your integration platform-wide. The suspension is platform-wide, but the notice is not a single delivery: one fires per affected (event, organization) tuple, exactly like integration.unpublished.

{
  "event_type":         "integration.suspended",
  "integration_id":     "int_yourapp",
  "organization_id":    "org_xyz789",
  "event_id":           "evt_abc123",
  "integration_status": "suspended_by_revento",
  "occurred_at":        "2026-05-09T18:00:00Z",
  "actor_id":           "usr_admin_123",
  "reason":             "Pending review of a reported credential leak"
}

integration_status carries the state the integration has moved into: suspended_by_revento for this notice. reason is optional free text.

There is no reason_code enum on this payload; the reason is free text or absent.

This is a notification; there is no API action to resume. Contact your account owner.

Dedup key: (integration_id, event_id, organization_id, occurred_at). Deduplicating on (integration_id, occurred_at) alone would collapse the notices for every one of your connections into one.