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 five rules in mind:

1. Ask me for my Veeting domain. Do not guess one.
2. The API key is a server side secret. 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.

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 it even if you skip the rest.

  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 and carry the real outcome 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 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: Instance-level keys must never be shared with customers. They 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, every call made with such a key is rejected.

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; any other target is rejected.

Identifiers

A meeting has two identifiers, and they are not interchangeable. Swapping them is the most common mistake made against 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. PUT /meeting happens to accept either, but the other endpoints do not, so use data.id everywhere and you will always be right.

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, 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 moment.
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","paidUntil":"2026-12-31T23:59:59.000Z"}'
{
  "responseCode": 0,
  "data": {
    "id": "5e9459c5b863b82cefdddf4f",
    "name": "test-user@example.com",
    "accountType": "trial",
    "numberOfMeetingRooms": 1,
    "paidUntil": "2026-12-31T23:59:59.000Z",
    "accountRooms": []
  }
}

data.id is the Account ID. Keep it; the next call needs it.

The response describes the account, not the administrator it created, so it does not contain 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 or duration is rarely what you intended.

FieldTypeRequiredNotes
topicstringyes
startTimeISO 8601yes
endTimeISO 8601no
durationnumbernoMinutes. It is not derived from the times, so make it agree with them yourself.
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 nullnonull uses the account default.

With an instance-level key, add X-USER-ID or X-USER-EMAIL to schedule on behalf of that organizer.

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":{},"meetingPermissionId":null}'
{
  "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": false,
    "isOpen": false,
    "isClosed": true,
    "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.
  • isClosed is true on a meeting you just created, and that is normal. It 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.

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 them to the meeting without writing to them.
"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:

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.
monthlyPatternday repeats on the same day of the month; nthWeekDay repeats 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. A value the platform does not recognize is dropped without an error, and a weekly series that ends up with no recognized day simply repeats on the day its startTime falls on. Nothing fails, so it is worth getting 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. Two 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 the meeting is password-protected, the invitation link also carries the password hash so the recipient does not have to type it:

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

Returns the same meeting object that POST /meeting returned. It accepts the 24-character id only.

You will need this more often than you might expect, because PUT replaces rather than patches: read the meeting, change the fields you care about, then send the whole object back. It is also how you reconcile after a web hook you may have missed, 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.

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.

This is a replace, not a patch. Send the complete meeting object, including an id field repeating the meeting ID, because fields you leave out are not preserved.

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":{},"meetingPermissionId":null}'

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 failed",
  "errors": []
}
responseCodeMeaningWhat to do
0Success
-1The platform is in a maintenance windowRetry later
-11Not foundCheck the ID, do not retry
-22Application errorDo not retry blindly; the call did not do what you asked
-33API errorCheck the request shape
-44Input validation failedFix the payload; errors says what failed
-55Billing errorThe account is not entitled to this action
-99Authentication failedBad, inactive, or unauthorized API key, or a user header the key is not allowed to 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 a duration that disagrees with startTime and endTime.
  • Sending local times without the Z suffix.
  • Reading isClosed: true on a newly created meeting as an error.
  • Sending only the changed fields to PUT /meeting, which clears the rest.
  • Putting the API key in client-side code.

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