The Veeting REST API lets your own system create accounts, schedule meetings on behalf of your users, and manage those meetings throughout their lifecycle. It is a plain JSON API over HTTPS, so any HTTP client works. The examples below use curl.
Base URL: https://<DOMAIN-NAME>/api/v6
<DOMAIN-NAME> is your own web meeting domain. There is no shared or global host, so use the domain your instance runs on.
Authentication: an X-API-KEY header on every request.
Content type: application/json on every POST and PUT.
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/api-usage
It documents the Veeting Rooms REST API. Keep these six rules in mind:
1. Ask me for my Veeting domain and my API key before you write any
code, unless I have already given them to you. Do not guess
either one.
2. The API key is a server side secret. Keep it in an environment
variable, never hardcoded in the source. If I ask for a browser
integration, build a backend for it instead and tell me why.
3. A call succeeded only if the HTTP status is 200 AND the body has
responseCode 0. Check both.
4. A meeting has two identifiers. Use the 24 character "id" in API
calls, and the dashed "meetingId" in the join URL. Never swap them.
5. Ask me whether my API key is account level or instance level. It
changes which endpoints and headers apply.
6. The list endpoints have no paging and no sorting, so sort the
array yourself. /meeting/past without both startDate and endDate
returns every meeting the account ever had, not just past ones.
What I want to build:These four rules apply to every endpoint on this page. The second one surprises most integrators, so read rule 2 even if you skip the others.
The API key is a server-side secret. Anyone who holds it has full control over your web meeting platform. Never put it in browser JavaScript, in a mobile app bundle, or in a public repository. If you need to start meetings from a browser, call the API from your own backend and pass only the result to the page.
A successful call returns HTTP 200 and responseCode: 0. Most endpoints return HTTP 200 even when the call failed. The real outcome is in a negative responseCode. Checking only the HTTP status makes failures look like successes. Check both.
Payload data is always nested under data. Read your values from data, never from the top level. An error response has no data field at all.
All timestamps are ISO 8601 in UTC, with a Z suffix. The timezone field on a meeting only affects how times are printed in invitations. It does not shift startTime or endTime.
There are two kinds of API key, and the one you hold determines what you can do.
| Account level | Instance level | |
|---|---|---|
| Created in | Account Settings | White label settings |
| Can create accounts | no | yes |
| Can manage meetings | yes, within its own account | yes, for any user on the instance |
| Permissions | always meetings only | selectable: accounts, users, meetings, reporting, branding |
| Who may hold it | the account owner | only the operator of the white-label instance |
Note: Never share an instance-level key with a customer. Such a key can create accounts and act for any user on the instance.
Account-level keys are always restricted to the meeting endpoints, no matter what else you try to grant them. The platform enforces that restriction rather than relying on convention, which makes an account-level key safe to issue to an integrator who only needs to schedule meetings.
Account-level keys also require the accountLevelApiKeys feature to be enabled on the white-label instance. If it is off, the platform rejects every call made with such a key.
By default, a key acts as a synthetic API user that does not belong to any person. To act as a real meeting organizer instead, send one of these headers:
X-USER-ID: <USER-ID>
X-USER-EMAIL: <USER-EMAIL>If you send both, X-USER-ID wins and X-USER-EMAIL is ignored, so send only one.
Both key types support these headers. An instance-level key can act as any user on the instance. An account-level key can act only as an organizer of its own account; the platform rejects any other target.
A meeting has two identifiers, and they are not interchangeable. Swapping them is the most common mistake integrators make with this API.
| Name | Looks like | What it is for |
|---|---|---|
data.id | 5e945c36b863b82cefdddf54 | 24-character hex. Use it in every API call. |
data.meetingId | 9974-7653-8886-0485 | Dashed digits. Use it in the join URL and when showing the meeting to people. |
Rule of thumb: if it has dashes, it is for people; if it is 24 hex characters, it is for the API. No endpoint accepts the dashed
meetingId, not evenPUT /meeting, so usedata.ideverywhere.
Three more identifiers appear in responses:
| Name | Source |
|---|---|
| Account ID | data.id of POST /account |
| User ID | data[].id of GET /account/admin/{accountId} |
| Room ID | data.roomId of a meeting; the room is created together with the account |
Creates an account and its first administrator in one call. Instance-level keys only.
| Field | Type | Required | Notes |
|---|---|---|---|
| adminEmail | string | yes | Must be unique across the whole instance. An email address can exist only once, even in different accounts. |
| adminFirstname | string | yes | |
| adminLastname | string | yes | |
| adminPreferredLanguage | string | yes | ISO 639-1, for example en or de |
| accountType | string | yes | trial, standard, business, professional, confidential, boardroom, classroom, boardroomClassroom, school, payAsYouGo, internal |
| sendPassword | boolean | yes | true emails a password to adminEmail. Use false while testing so you do not mail real people. |
| paidUntil | ISO 8601 | no | The account stops working after this time. For trial and payAsYouGo the value you send is not stored: a trial ends 14 days from now (30 if you send a date more than 30 days before or after today), and payAsYouGo one year from now. |
curl 'https://<DOMAIN-NAME>/api/v6/account' \
-X POST \
-H 'X-API-KEY: <API-KEY>' \
-H 'content-type: application/json' \
--data-binary '{"sendPassword":true,"accountType":"trial","adminFirstname":"Test","adminLastname":"User","adminEmail":"test-user@example.com","adminPreferredLanguage":"en"}'{
"responseCode": 0,
"data": {
"id": "5e9459c5b863b82cefdddf4f",
"name": "test-user@example.com",
"accountType": "trial",
"numberOfMeetingRooms": 1,
"paidUntil": "2026-09-21T09:00:00.000Z",
"accountRooms": []
}
}data.id is the Account ID. Keep it; the next call needs it.
The response describes the account, not the administrator that POST /account created, so it does not include that person's User ID. Fetch it with the next call.
Returns the administrators of an account. Instance-level keys only. This is how you obtain the User ID of the administrator that POST /account created.
curl 'https://<DOMAIN-NAME>/api/v6/account/admin/<ACCOUNT-ID>' \
-X GET \
-H 'X-API-KEY: <API-KEY>'{
"responseCode": 0,
"data": [
{
"id": "5e9459c5b863b82cefdddf4e",
"email": "test-user@example.com",
"firstName": "Test",
"lastName": "User",
"preferredLanguage": "en",
"timezone": "Europe/Zurich"
}
]
}data is an array. On a freshly created account, it holds one entry, but an account that has been in use can have several, so match on the email address you expect rather than always taking data[0].
Only two fields are required. The rest are optional, and the platform fills in account defaults for anything you leave out. The example below sends the full set, which is usually what you want: a meeting with no endTime and no duration is 60 minutes long, which is rarely what you intended.
| Field | Type | Required | Notes |
|---|---|---|---|
| topic | string | yes | |
| startTime | ISO 8601 | yes | |
| endTime | ISO 8601 | no | |
| duration | number | no | Minutes. Send duration or endTime, not both: duration wins and overwrites endTime. Send neither and the meeting is 60 minutes long. |
| type | string | no | standard, offTheRecord, boardroom, classroom, audiobridge |
| isRecurring | boolean | no | |
| recurring | object | no | Send {} when isRecurring is false. See below for the schema. |
| isRecorded | boolean | no | Recording is only available for certain meeting types. |
| isDialin | boolean | no | |
| invitedParticipants | array | no | [] for none. See below for the shape of an entry. |
| meetingPermissionId | string or null | no | Omit it to apply the current account's default permission set. Send null or "" to create the meeting with no permission set. |
With an instance-level key, add X-USER-ID or X-USER-EMAIL to schedule the meeting on behalf of the user the header names.
curl 'https://<DOMAIN-NAME>/api/v6/meeting' \
-X POST \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-ID: <USER-ID>' \
-H 'content-type: application/json' \
--data-binary '{"topic":"My Meeting Topic","startTime":"2026-09-07T09:00:00.000Z","endTime":"2026-09-07T10:00:00.000Z","duration":60,"type":"standard","isRecurring":false,"isRecorded":false,"isDialin":false,"invitedParticipants":[],"recurring":{}}'{
"responseCode": 0,
"data": {
"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",
"type": "standard",
"roomId": "5e9459c5b863b82cefdddf50",
"isActive": true,
"isOpen": false,
"isClosed": false,
"accountId": "5e9459c5b863b82cefdddf4f",
"addedByUserId": "5e9459c5b863b82cefdddf4e",
"timezone": "Europe/Zurich"
}
}Reading the response:
data.id is what every later call needs.data.meetingId is what you show people, and what goes in the join URL.isOpen is false on a meeting you just created, and that is normal. isOpen: false means nobody can join yet. A meeting opens for moderators about an hour before it starts (isPreOpen) and for everyone about fifteen minutes before (isOpen). Do not treat it as a failed call and do not retry.isClosed becomes true once the meeting has ended or was closed. If it is true right after creation, the meeting's end (endTime, or startTime plus duration) is already in the past.Each entry in invitedParticipants is an object:
| Field | Type | Required | Notes |
|---|---|---|---|
| string | yes | Must be a valid email address. | |
| name | string | no | Shown in the invitation. |
| sendInvite | boolean | yes | true emails this person an invitation with the join link. false adds the person to the meeting without sending an email. |
"invitedParticipants": [
{ "email": "anna@example.com", "name": "Anna Meier", "sendInvite": true }
]Set sendInvite to false while testing, for the same reason as sendPassword on account creation: it is the difference between a test run and mailing a real person.
Send {} when isRecurring is false. When it is true, these fields apply:
| Field | Values |
|---|---|
| frequencyType | never, daily, weekly, monthly, yearly |
| frequency | Number. How many units between occurrences: 2 with weekly means every other week. Capped at 52 for weekly, 12 for monthly, 10 for yearly. |
| weekDays | Which days a weekly series falls on, as two-letter codes: MO, TU, WE, TH, FR, SA, SU. |
| monthlyPattern | With day the series repeats on the same day of the month; with nthWeekDay, on the same weekday of the month, for example the second Tuesday. |
| endsType | never, after, on |
| endsAfter | Number of occurrences, when endsType is after. The count includes the first occurrence, so 12 gives twelve meetings in total. Capped at 365. |
| endsOn | ISO 8601 date, when endsType is on |
| exclude | Array of ISO 8601 dates to skip |
weekDaystakesMO, notMonday. An unrecognized entry next to valid codes is ignored. If no entry is a valid two-letter code, the series is created without an error but has no occurrences at all: it never appears in meeting lists and never opens. Nothing fails, so it is worth gettingweekDaysright the first time.
A weekly standup, every Monday, twelve times:
{
"isRecurring": true,
"recurring": {
"frequencyType": "weekly",
"frequency": 1,
"weekDays": ["MO"],
"endsType": "after",
"endsAfter": 12
}
}The API does not return a link. Build it yourself from the dashed meetingId:
https://<DOMAIN-NAME>/meeting/<data.meetingId>For the example above, that is https://<DOMAIN-NAME>/meeting/9974-7653-8886-0485.
This is the same URL that Veeting puts in the invitation emails and calendar entries it sends. Three things to know about it:
meetingId. The 24-character id does not work here.<DOMAIN-NAME> you call the API on, but a white-label instance can be configured with a different one, so take it from your own configuration rather than assuming.meetingPermissionId or meetingRegistrationFormId as null or "" then whatever value was previously set will be removed. If the fields are missing we will keep using the existing values.If the meeting is password-protected and includeMeetingPasswordInMeetingLink is true (set on the meeting or taken from the account default), the invitation link also carries the password hash, so the recipient does not have to type the password. Otherwise data.passwordHash is null and the link has no hash. With the hash, the link looks like this:
https://<DOMAIN-NAME>/meeting/<data.meetingId>?meetingAccessHash=<data.passwordHash>You can add query parameters to a join URL to preset the participant name, skip the device test, choose a layout, and more.
curl 'https://<DOMAIN-NAME>/api/v6/meeting/5e945c36b863b82cefdddf54' \
-X GET \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>'The response is the stored meeting without the computed state fields: isActive, isOpen, isClosed, isPreOpen and isPasswordProtected are absent. The endpoint accepts the 24-character id only. If the ID is valid but the meeting does not exist or you may not read it, the call returns responseCode: 0 with data: null, not an error.
You will need this call more often than you expect, because the safe way to update a meeting is to read it, change the fields you care about, then send the whole object back: a PUT that leaves out invitedParticipants or password removes the participants or the password. It is also how you catch up on a meeting's state after a missed web hook, since web hook delivery is attempted once with no retry.
The caller must be an organizer of the account that owns the meeting or be listed in its invitedParticipants.
Three endpoints return lists of meetings. There is no GET /meeting, no /meeting/list, no /meetings, and no /history. These three are all there is.
| Route | Covers |
|---|---|
GET /meeting/upcoming | From 00:00 UTC today forward, with no upper bound |
GET /meeting/past | Meetings that have already ended, within a date range |
GET /meeting/<START>/<END> | Any window you name, past or future |
Every meeting from 00:00:00 UTC of the current day onward. With no scheduled meetings, the endpoint returns [].
curl 'https://<DOMAIN-NAME>/api/v6/meeting/upcoming' \
-X GET \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>'{
"responseCode": 0,
"data": [
{
"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",
"type": "standard",
"roomId": "5e9459c5b863b82cefdddf50",
"isActive": true,
"isOpen": false,
"isClosed": false,
"accountId": "5e9459c5b863b82cefdddf4f",
"addedByUserId": "5e9459c5b863b82cefdddf4e",
"timezone": "Europe/Zurich"
}
]
}Meetings that have already ended, within the range you ask for. Range membership is decided by when the meeting ended: the close time if it was closed early, otherwise its endTime. A range in the future returns [].
curl 'https://<DOMAIN-NAME>/api/v6/meeting/past?startDate=2026-08-01T00:00:00Z&endDate=2026-08-31T23:59:59Z' \
-X GET \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>'The endpoint takes these optional query parameters. Without startDate and endDate, the endpoint returns every meeting the account has ever had, not only past ones:
| Parameter | Behavior |
|---|---|
startDate | ISO 8601. Snapped down to 00:00:00.000 UTC of that day. |
endDate | ISO 8601. Snapped up to 23:59:59.999 UTC of that day. |
topic | Case-insensitive substring match on the topic. Punctuation matches literally. |
email | Address of a participant. It filters only part of the result, so do not rely on it: filter by participant in your own code. |
recorded | Only the exact string true filters to recorded meetings. TRUE, 1, and anything else count as false. |
tags | Comma-separated tag names. Returns meetings with at least one of these tags, ignoring case. |
The counterpart to the two endpoints above, for a window you choose yourself. Same response shape, arbitrary range.
GET /api/v6/meeting/<START>/<END>Both path segments are required. Pass the literal string null in one segment to leave that end open. An unparsable segment behaves exactly like null. Passing null in both segments does not return everything: with neither bound set, the endpoint returns an empty array.
Unlike /meeting/past, this endpoint does not restrict the result to finished meetings. It returns whatever falls inside the window, past or future, and membership is decided by endTime alone, so a meeting that is currently running is included.
curl 'https://<DOMAIN-NAME>/api/v6/meeting/2026-08-01T00:00:00Z/null' \
-X GET \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>'PUT /api/v6/meeting/<MEETING-ID>/<SEND-UPDATE-TO-PARTICIPANTS>The second path segment is a boolean, and it is required:
true sends an updated invitation to everyone in invitedParticipants.false changes the meeting quietly.Send id in the body too; it must equal the meeting ID in the path. Most fields you omit, or send as null, keep their current value. Two do not:
invitedParticipants and every invited participant is removed.password, or send it empty, and the password is removed: the meeting is no longer password-protected.To clear meetingPermissionId or meetingRegistrationFormId, send an empty string ""; null keeps the current value.
The safe pattern is read-modify-write: read the meeting back, change what you need, and send the whole object.
curl 'https://<DOMAIN-NAME>/api/v6/meeting/5e945c36b863b82cefdddf54/false' \
-X PUT \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>' \
-H 'content-type: application/json' \
--data-binary '{"id":"5e945c36b863b82cefdddf54","topic":"My Updated Meeting Topic","startTime":"2026-09-07T09:30:00.000Z","endTime":"2026-09-07T10:30:00.000Z","duration":60,"type":"standard","isRecurring":false,"isRecorded":false,"isDialin":false,"invitedParticipants":[],"recurring":{}}'Meetings close by themselves when they end. Close one early to end it immediately: anyone still in the room is removed.
Closing is not deleting. The meeting record stays.
curl 'https://<DOMAIN-NAME>/api/v6/meeting/close' \
-X POST \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>' \
-H 'content-type: application/json' \
--data-binary '{"meetingId":"5e945c36b863b82cefdddf54"}'Note: Despite its name, the
meetingIdfield in this body takes the 24-characterdata.id, not the dasheddata.meetingId.
curl 'https://<DOMAIN-NAME>/api/v6/meeting/5e945c36b863b82cefdddf54' \
-X DELETE \
-H 'X-API-KEY: <API-KEY>' \
-H 'X-USER-EMAIL: <USER-EMAIL>'You can delete several meetings in one call by separating their IDs with commas:
DELETE /api/v6/meeting/<ID-1>,<ID-2>,<ID-3>You can include a cancellationMessage in the body; it appears in the cancellation notice sent to the invited participants.
An error response carries a negative responseCode and a responseMessage, and has no data field. Remember that the HTTP status is usually still 200.
{
"responseCode": -44,
"responseMessage": "Input validation errors"
}| responseCode | Meaning | What to do |
|---|---|---|
| 0 | Success | |
| -1 | The platform is in a maintenance window | Retry later |
| -11 | Not found, or unreadable request | Check the ID and the route. With HTTP 500, the body was malformed JSON or over 50 KB |
| -33 | API error | Read responseMessage: a refusal such as not allowed or room occupied, or "System error" if we failed |
| -44 | Input validation failed | Fix the payload against the field tables on this page |
| -55 | Billing error | The account is not entitled to this action |
| -99 | Authentication failed | Bad, inactive, or unauthorized API key, or a user header naming someone the key may not act for. Do not retry |
Requests are rate-limited per IP address at 50 per second, with a short burst allowance. Above that, the platform returns HTTP 429 rather than a responseCode.
responseCode.meetingId where the 24-character id is required.id instead of meetingId.X-USER-ID together with X-USER-EMAIL and expecting the email to be used.POST /account with an account-level key.duration and endTime and expecting endTime to survive. duration overwrites it.Z suffix.isOpen: false on a newly created meeting as an error.PUT /meeting, which removes the invited participants and the password./meeting/past without startDate and endDate and expecting only past meetings.2026-08-01 and expecting it to mean UTC midnight.01.08.2026.id as unique when every occurrence of a recurring series carries the same one./meeting/null/null to fetch every meeting.X-USER-ID and X-USER-EMAIL on the list endpoints with an instance-level key.With an instance-level key, provisioning a customer and scheduling their first meeting looks like this:
POST /account -> data.id Account ID
GET /account/admin/<ACCOUNT-ID> -> data[0].id User ID
POST /meeting + X-USER-ID -> data.id use in API calls
data.meetingId use in the join URL
GET /meeting/upcoming -> data[] from today, unsorted
GET /meeting/past + date range -> data[] finished meetings
PUT /meeting/<ID>/<true|false> update
POST /meeting/close end early
DELETE /meeting/<ID> removeWith an account-level key, skip the first two steps and post straight to /meeting.
Contact our team to discuss the details.