Veeting Rooms REST APIs

Overview

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.

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/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:

Rules that apply to every request

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Choosing an API key

There are two kinds of API key, and the one you hold determines what you can do.

Account levelInstance level
Created inAccount SettingsWhite label settings
Can create accountsnoyes
Can manage meetingsyes, within its own accountyes, for any user on the instance
Permissionsalways meetings onlyselectable: accounts, users, meetings, reporting, branding
Who may hold itthe account owneronly 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.

Acting on behalf of a user

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.

Identifiers

A meeting has two identifiers, and they are not interchangeable. Swapping them is the most common mistake integrators make with this API.

NameLooks likeWhat it is for
data.id5e945c36b863b82cefdddf5424-character hex. Use it in every API call.
data.meetingId9974-7653-8886-0485Dashed 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 even PUT /meeting, so use data.id everywhere.

Three more identifiers appear in responses:

NameSource
Account IDdata.id of POST /account
User IDdata[].id of GET /account/admin/{accountId}
Room IDdata.roomId of a meeting; the room is created together with the account

Create an account

Creates an account and its first administrator in one call. Instance-level keys only.

FieldTypeRequiredNotes
adminEmailstringyesMust be unique across the whole instance. An email address can exist only once, even in different accounts.
adminFirstnamestringyes
adminLastnamestringyes
adminPreferredLanguagestringyesISO 639-1, for example en or de
accountTypestringyestrial, standard, business, professional, confidential, boardroom, classroom, boardroomClassroom, school, payAsYouGo, internal
sendPasswordbooleanyestrue emails a password to adminEmail. Use false while testing so you do not mail real people.
paidUntilISO 8601noThe 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.

Retrieve the account administrator

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].

Create a meeting

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.

FieldTypeRequiredNotes
topicstringyes
startTimeISO 8601yes
endTimeISO 8601no
durationnumbernoMinutes. Send duration or endTime, not both: duration wins and overwrites endTime. Send neither and the meeting is 60 minutes long.
typestringnostandard, offTheRecord, boardroom, classroom, audiobridge
isRecurringbooleanno
recurringobjectnoSend {} when isRecurring is false. See below for the schema.
isRecordedbooleannoRecording is only available for certain meeting types.
isDialinbooleanno
invitedParticipantsarrayno[] for none. See below for the shape of an entry.
meetingPermissionIdstring or nullnoOmit 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.

Inviting participants

Each entry in invitedParticipants is an object:

FieldTypeRequiredNotes
emailstringyesMust be a valid email address.
namestringnoShown in the invitation.
sendInvitebooleanyestrue 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.

The recurring object

Send {} when isRecurring is false. When it is true, these fields apply:

FieldValues
frequencyTypenever, daily, weekly, monthly, yearly
frequencyNumber. How many units between occurrences: 2 with weekly means every other week. Capped at 52 for weekly, 12 for monthly, 10 for yearly.
weekDaysWhich days a weekly series falls on, as two-letter codes: MO, TU, WE, TH, FR, SA, SU.
monthlyPatternWith 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.
endsTypenever, after, on
endsAfterNumber of occurrences, when endsType is after. The count includes the first occurrence, so 12 gives twelve meetings in total. Capped at 365.
endsOnISO 8601 date, when endsType is on
excludeArray of ISO 8601 dates to skip

weekDays takes MO, not Monday. 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 getting weekDays right the first time.

A weekly standup, every Monday, twelve times:

{
  "isRecurring": true,
  "recurring": {
    "frequencyType": "weekly",
    "frequency": 1,
    "weekDays": ["MO"],
    "endsType": "after",
    "endsAfter": 12
  }
}

The join URL

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:

  • It uses the dashed meetingId. The 24-character id does not work here.
  • The host is the domain your users open the meeting room on. On most instances, that is the same <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.
  • If you send 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.

Read a meeting back

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.

List meetings

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.

RouteCovers
GET /meeting/upcomingFrom 00:00 UTC today forward, with no upper bound
GET /meeting/pastMeetings that have already ended, within a date range
GET /meeting/<START>/<END>Any window you name, past or future

Upcoming meetings

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"
    }
  ]
}

Past meetings

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:

ParameterBehavior
startDateISO 8601. Snapped down to 00:00:00.000 UTC of that day.
endDateISO 8601. Snapped up to 23:59:59.999 UTC of that day.
topicCase-insensitive substring match on the topic. Punctuation matches literally.
emailAddress of a participant. It filters only part of the result, so do not rely on it: filter by participant in your own code.
recordedOnly the exact string true filters to recorded meetings. TRUE, 1, and anything else count as false.
tagsComma-separated tag names. Returns meetings with at least one of these tags, ignoring case.

Meetings in any period

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>'

Update a meeting

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:

  • Omit invitedParticipants and every invited participant is removed.
  • Omit 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":{}}'

Close a meeting early

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 meetingId field in this body takes the 24-character data.id, not the dashed data.meetingId.

Delete meetings

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.

Errors

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"
}
responseCodeMeaningWhat to do
0Success
-1The platform is in a maintenance windowRetry later
-11Not found, or unreadable requestCheck the ID and the route. With HTTP 500, the body was malformed JSON or over 50 KB
-33API errorRead responseMessage: a refusal such as not allowed or room occupied, or "System error" if we failed
-44Input validation failedFix the payload against the field tables on this page
-55Billing errorThe account is not entitled to this action
-99Authentication failedBad, 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.

Common mistakes

  • Treating HTTP 200 as success without checking responseCode.
  • Passing the dashed meetingId where the 24-character id is required.
  • Building the join URL with id instead of meetingId.
  • Sending X-USER-ID together with X-USER-EMAIL and expecting the email to be used.
  • Calling POST /account with an account-level key.
  • Sending both duration and endTime and expecting endTime to survive. duration overwrites it.
  • Sending local times without the Z suffix.
  • Reading isOpen: false on a newly created meeting as an error.
  • Sending only the changed fields to PUT /meeting, which removes the invited participants and the password.
  • Putting the API key in client-side code.
  • Expecting paging or a stable order from the list endpoints.
  • Calling /meeting/past without startDate and endDate and expecting only past meetings.
  • Sending a bare date such as 2026-08-01 and expecting it to mean UTC midnight.
  • Sending a day-first date such as 01.08.2026.
  • Treating a list id as unique when every occurrence of a recurring series carries the same one.
  • Calling /meeting/null/null to fetch every meeting.
  • Omitting X-USER-ID and X-USER-EMAIL on the list endpoints with an instance-level key.

End-to-end

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>                  remove

With an account-level key, skip the first two steps and post straight to /meeting.

Not sure how to best implement your project?

Contact our team to discuss the details.