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:
If you configure both, your endpoint is called twice for the same event, once from each level.
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: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.
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.
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.
Check the payload shape before reading it. Some events send a single object and some send an array. They are listed below.
Each web hook is configured separately, with four settings:
| Setting | Meaning |
|---|---|
| enabled | Whether this web hook fires at all |
| url | The endpoint we call |
| method | POST, PUT, or DELETE. Empty or unrecognized values fall back to POST. |
| apiKey | Optional. See below. |
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.
There are fourteen events.
| Event | Fires when | Payload |
|---|---|---|
onMeetingScheduled | A meeting is created | Meeting |
onMeetingUpdated | A meeting is changed | Meeting |
onMeetingClosed | A meeting closes, automatically or manually | Meeting |
onMeetingDeleted | Meetings are deleted | Array of Meeting |
onMeetingCancelled | A single occurrence of a recurring meeting is canceled | { meeting, day } |
onMeetingJoined | Somebody joins a meeting | MeetingParticipantsStatus |
onMeetingLeft | Somebody leaves a meeting | MeetingParticipantsStatus |
onMeetingSummaryCreated | A meeting summary is created, usually about five minutes after the meeting closes | MeetingSummary, wrapped |
onMeetingRecordingCreated | A recorded meeting has been merged | Recording |
onMeetingsReminder15MinutesBefore | 15 minutes before meetings start | Array of Meeting |
onMeetingsReminder30MinutesBefore | 30 minutes before meetings start | Array of Meeting |
onMeetingsReminder60MinutesBefore | 60 minutes before meetings start | Array of Meeting |
onMeetingsReminder24HoursBefore | 24 hours before meetings start | Array of Meeting |
onMeetingsReminder48HoursBefore | 48 hours before meetings start | Array of Meeting |
Two things about the reminders are easy to miss:
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.
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.
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"
}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,
meetingIdholds the 24-character meetingid, andmeetingTokenholds the dashed number. That is the opposite of the naming used everywhere else, so read these fields carefully.
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:
| Field | What it measures |
|---|---|
| meetingConversationDuration | How long at least two people were in the room at the same time |
| meetingConversationStartToEndDuration | From the first time two people were in the room together until the last |
| meetingParticipantsStartToEndDuration | From the first person joining to the last person leaving |
| meetingStartToEndDuration | The scheduled duration, including any extension |
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"]
}| Property | Meaning |
|---|---|
| meeting | The full meeting object, as described above |
| summary | The meeting summary this recording belongs to |
| mergedRecordingUrl | The single merged recording |
| recordingUrls | The individual parts that were merged, as the recording host reports them |
| emailNotificationRecipients | Who the platform is about to email about this recording |
Note: read the meeting's identifiers from
meeting.idandmeeting.meetingId. There is nomeetingIdat the top level of this payload.
onMeetingSummaryCreated as a bare object. It is the one wrapped payload.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.apiKey empty on an endpoint that changes state.Contact our team to discuss the details.