Web hooks

Overview

Web hooks let Veeting call your system when something happens in a meeting, so you do not have to poll the API. When an event fires, the platform sends an HTTP request to a URL you configure, with the affected object as the JSON body.

Web hooks are configured in two places, and both fire for the same event:

  • White-label instance level, in the "Web hooks" section of the System Configuration. Fires for every account on the instance.
  • Account level, per account. Fires only for that account.

If you configure both, your endpoint is called twice for the same event, once from each level.

Using this page with an AI assistant

If you are using an AI coding assistant, copy the text below into it and add a sentence describing what you want to build. AI assistants accessing this page receive a separate, token-optimized version of our full developer documentation, specifically tailored by our engineers for AI coding assistants.

Before writing any code, read this page:
https://www.veeting.com/en/developer-documentation/web-hooks

It documents the Veeting Rooms web hooks. Keep these five rules in mind:

1. Delivery is attempted once. There is no retry. Acknowledge fast,
   queue the work, and reconcile through the REST API if it matters.
2. Payloads are NOT wrapped in responseCode and data, except
   onMeetingSummaryCreated, which is.
3. Some events send an array, not an object. All five reminders and
   onMeetingDeleted do. Check before reading fields.
4. Authentication is one optional X-API-KEY header. If I want the
   endpoint protected, verify that header and reject anything else.
5. Ask me whether the web hook is configured at instance level or
   account level. Both can be on, which means two calls per event.

What I want to build:

Rules that apply to every web hook

  1. Delivery is attempted once. There is no retry, no backoff, and no dead-letter queue. If your endpoint is down, times out, or answers with an error status, the event is logged on our side and lost on yours. Design for that: acknowledge quickly, queue the work internally, and if you need certainty, read the meeting back with GET /meeting/{id} rather than trusting that every event arrived.

  2. Answer quickly. No delivery timeout is configured, so a slow endpoint holds a connection open rather than failing fast. Return a 2xx as soon as you have accepted the payload, then do the work.

  3. The body is the object, not an envelope. Unlike REST API responses, web hook payloads are not wrapped in responseCode and data. Two events differ: onMeetingSummaryCreated arrives wrapped in responseCode and data, and onMeetingRecordingCreated arrives as an object with the meeting nested inside it rather than as the meeting itself. Both are described below.

  4. Check the payload shape before reading it. Some events send a single object and some send an array. They are listed below.

Configuration

Each web hook is configured separately, with four settings:

SettingMeaning
enabledWhether this web hook fires at all
urlThe endpoint we call
methodPOST, PUT, or DELETE. Empty or unrecognized values fall back to POST.
apiKeyOptional. See below.

Protecting your endpoint

Set apiKey and the platform sends it as an X-API-KEY header on every call, so your endpoint can reject requests that do not carry it. If you leave it empty, no header is sent.

This is the only authentication on a web hook call. An endpoint without an API key can be called by anyone who learns its URL.

content-type: application/json is always sent.

The events

There are fourteen events.

EventFires whenPayload
onMeetingScheduledA meeting is createdMeeting
onMeetingUpdatedA meeting is changedMeeting
onMeetingClosedA meeting closes, automatically or manuallyMeeting
onMeetingDeletedMeetings are deletedArray of Meeting
onMeetingCancelledA single occurrence of a recurring meeting is canceled{ meeting, day }
onMeetingJoinedSomebody joins a meetingMeetingParticipantsStatus
onMeetingLeftSomebody leaves a meetingMeetingParticipantsStatus
onMeetingSummaryCreatedA meeting summary is created, usually about five minutes after the meeting closesMeetingSummary, wrapped
onMeetingRecordingCreatedA recorded meeting has been mergedRecording
onMeetingsReminder15MinutesBefore15 minutes before meetings startArray of Meeting
onMeetingsReminder30MinutesBefore30 minutes before meetings startArray of Meeting
onMeetingsReminder60MinutesBefore60 minutes before meetings startArray of Meeting
onMeetingsReminder24HoursBefore24 hours before meetings startArray of Meeting
onMeetingsReminder48HoursBefore48 hours before meetings startArray of Meeting

Two things about the reminders are easy to miss:

  • They deliver an array because one reminder run covers every meeting due at that moment. Even a single meeting arrives as an array of one.
  • The scope differs by level. A white-label instance reminder receives every due meeting on the instance. An account-level reminder receives only that account's meetings, so the same run sends different arrays to different endpoints.

Payloads

Meeting

Sent by onMeetingScheduled, onMeetingUpdated, and onMeetingClosed. The same object the REST API returns from POST /meeting, abridged here:

{
  "id": "5e945c36b863b82cefdddf54",
  "meetingId": "9974-7653-8886-0485",
  "topic": "My Meeting Topic",
  "startTime": "2026-09-07T09:00:00.000Z",
  "endTime": "2026-09-07T10:00:00.000Z",
  "duration": 60,
  "type": "standard",
  "roomId": "5e9459c5b863b82cefdddf50",
  "isRecurring": false,
  "isRecorded": false,
  "isDialin": false,
  "invitedParticipants": [],
  "agenda": "",
  "whitelabelId": "5c737902b377b0f7fbf81fce",
  "accountId": "5e9459c5b863b82cefdddf4f",
  "addedByUserId": "5e9459c5b863b82cefdddf4e",
  "addedByUserEmail": "test-user@example.com",
  "addedByUserName": "Test User",
  "timezone": "Europe/Zurich",
  "isActive": false,
  "isOpen": false,
  "isClosed": true
}

The two identifiers behave exactly as they do in the REST API: use id when calling back into the API, and meetingId when building a link or showing the meeting to somebody.

Array of Meeting

Sent by onMeetingDeleted and by all five reminders. The same object as above, in an array:

[
  { "id": "5e945c36b863b82cefdddf54", "meetingId": "9974-7653-8886-0485", "topic": "My Meeting Topic" }
]

On onMeetingDeleted, each meeting may also carry a cancellationMessage field with the text the organizer supplied when deleting the meeting.

Canceled occurrence

Sent by onMeetingCancelled when one occurrence of a recurring meeting is canceled rather than the whole series. The meeting is nested, and day identifies the occurrence:

{
  "meeting": { "id": "5e945c36b863b82cefdddf54", "meetingId": "9974-7653-8886-0485", "isRecurring": true },
  "day": "2026-09-14"
}

MeetingParticipantsStatus

Sent by onMeetingJoined and onMeetingLeft:

{
  "meetingId": "5df78199c015b37195230596",
  "meetingToken": "0000-0000-0000-0000",
  "isNamedRoom": false,
  "participant": {
    "name": "Joe Doe",
    "email": "joe@example.com",
    "meetingParticipantId": "4edaf0d2-cb6c-42f1-a266-4ae7d971f2e6",
    "isModerator": false
  },
  "totalNumberOfGuests": 3,
  "totalNumberOfModerators": 1,
  "whitelabelId": "5c737902b377b0f7fbf81fce",
  "accountId": "5e9459c5b863b82cefdddf4f"
}

Note: in this payload, meetingId holds the 24-character meeting id, and meetingToken holds the dashed number. That is the opposite of the naming used everywhere else, so read these fields carefully.

MeetingSummary

Sent by onMeetingSummaryCreated. This is the one payload delivered wrapped in responseCode and data:

{
  "responseCode": 0,
  "data": {
    "id": "5f7d49eb62e8f9a43b23f986",
    "meetingId": "5f7d49e662e8f9a43b23f983",
    "meetingType": "boardroom",
    "agenda": "",
    "minutes": "",
    "participants": [
      {
        "name": "Participant 1",
        "durations": [
          { "action": "joined", "timestamp": 1602046445811 },
          { "action": "left", "timestamp": 1602046670808 }
        ],
        "info": {
          "browserName": "Chrome",
          "browserVersion": "82.0.4062.0",
          "osName": "macOS",
          "osVersion": "10.15.6",
          "platformType": "desktop",
          "platformVendor": "Apple"
        }
      }
    ],
    "documents": [],
    "pdf": null,
    "accountId": "5c73790ab377b0f7fbf81fde",
    "isNamedRoom": false,
    "isRecordingPrepared": false,
    "sfuHostname": "ch-01-sfu-02.wlvmr.net",
    "meetingQuestions": [],
    "meetingConversationDuration": 25,
    "meetingConversationStartToEndDuration": 25,
    "meetingParticipantsStartToEndDuration": 35,
    "meetingStartToEndDuration": 60
  }
}

The four duration fields measure different things and are all in seconds:

FieldWhat it measures
meetingConversationDurationHow long at least two people were in the room at the same time
meetingConversationStartToEndDurationFrom the first time two people were in the room together until the last
meetingParticipantsStartToEndDurationFrom the first person joining to the last person leaving
meetingStartToEndDurationThe scheduled duration, including any extension

Recording

Sent by onMeetingRecordingCreated. Unlike the other payloads, this one is a wrapper: the meeting and the summary are nested inside it rather than forming the body themselves.

{
  "meeting": { "id": "5e945c36b863b82cefdddf54", "meetingId": "9974-7653-8886-0485", "topic": "My Meeting Topic" },
  "summary": { "id": "5f48a701ee0c388a879bffa7" },
  "mergedRecordingUrl": "https://<recording-host>/prepared/<meeting>/<token>.webm",
  "recordingUrls": [
    "video-file-1.webm",
    "video-file-2.webm",
    "video-file-3.webm"
  ],
  "emailNotificationRecipients": ["organizer@example.com"]
}
PropertyMeaning
meetingThe full meeting object, as described above
summaryThe meeting summary this recording belongs to
mergedRecordingUrlThe single merged recording
recordingUrlsThe individual parts that were merged, as the recording host reports them
emailNotificationRecipientsWho the platform is about to email about this recording

Note: read the meeting's identifiers from meeting.id and meeting.meetingId. There is no meetingId at the top level of this payload.

Common mistakes

  • Assuming a failed delivery will be retried. It will not.
  • Treating a reminder payload as a single meeting. All five reminders send an array.
  • Reading onMeetingSummaryCreated as a bare object. It is the one wrapped payload.
  • Reading meetingId in the join and leave payloads as the dashed number. In those payloads it is the 24-character id, and meetingToken holds the dashed number.
  • Configuring the same endpoint at both instance and account level and then treating the duplicate call as a bug.
  • Leaving apiKey empty on an endpoint that changes state.
  • Doing slow work before answering, because nothing times out the call for you.

Not sure how to best implement your project?

Contact our team to discuss the details.