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.

You configure web hooks in two places, and both levels 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. Veeting attempts delivery exactly 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. The one
   exception is onMeetingSummaryCreated.
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 or responds 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 the 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. Three events differ: onMeetingCancelled nests the meeting in the body, 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. The event table below says which.

Configuration

You configure each web hook 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 the value as an X-API-KEY header on every call, so your endpoint can reject any request that arrives without that header. If you leave it empty, no header is sent.

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

The platform always sends content-type: application/json.

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 finished mergingRecording
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 reminder configured at instance level carries every due meeting on the instance; a reminder configured at account level carries only that account's meetings. The same run therefore 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" }
]

In the onMeetingDeleted payload, 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 in the body, 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 such moment
meetingParticipantsStartToEndDurationFrom the first person joining to the last person leaving
meetingStartToEndDurationThe scheduled duration, including any extension

Recording

Sent by onMeetingRecordingCreated. This payload 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 will notify by 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.