# Veeting Rooms REST APIs

Source: https://www.veeting.com/en/developer-documentation/api-usage

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

```text
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 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:** 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:

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

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

| 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, created together with the account |

## Create an 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 moment.                                                                                                        |

```bash
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"}'
```

```json
{
  "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.

```bash
curl 'https://<DOMAIN-NAME>/api/v6/account/admin/<ACCOUNT-ID>' \
  -X GET \
  -H 'X-API-KEY: <API-KEY>'
```

```json
{
  "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.

| Field               | Type           | Required | Notes                                                                           |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| topic               | string         | **yes**  |                                                                                 |
| startTime           | ISO 8601       | **yes**  |                                                                                 |
| endTime             | ISO 8601       | no       |                                                                                 |
| duration            | number         | no       | Minutes. It is not derived from the times, so make it agree with them yourself. |
| 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       | `null` uses the account default.                                                |

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

```bash
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}'
```

```json
{
  "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:

| Field      | Type    | Required | Notes                                                                                                                 |
| ---------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| email      | 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 them to the meeting without writing to them. |

```json
"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`:

| 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 | `day` repeats on the same day of the month; `nthWeekDay` repeats 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                                                                                                                    |

> **`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:

```json
{
  "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`:

```text
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:

```text
https://<DOMAIN-NAME>/meeting/<data.meetingId>?meetingAccessHash=<data.passwordHash>
```

You can add [query parameters](/en/developer-documentation/query-parameters) to a join URL to preset the participant name, skip the device test, choose a layout, and more.

## Read a meeting back

```bash
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](/en/developer-documentation/web-hooks) 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

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

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

```bash
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

```bash
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:

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

```json
{
  "responseCode": -44,
  "responseMessage": "Input validation failed",
  "errors": []
}
```

| responseCode | Meaning                                 | What to do                                                                                               |
| ------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| 0            | Success                                 |                                                                                                          |
| -1           | The platform is in a maintenance window | Retry later                                                                                              |
| -11          | Not found                               | Check the ID, do not retry                                                                               |
| -22          | Application error                       | Do not retry blindly; the call did not do what you asked                                                 |
| -33          | API error                               | Check the request shape                                                                                  |
| -44          | Input validation failed                 | Fix the payload; `errors` says what failed                                                               |
| -55          | Billing error                           | The account is not entitled to this action                                                               |
| -99          | Authentication failed                   | Bad, 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:

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

---

## The rest of this documentation

- [Custom tools](https://www.veeting.com/en/developer-documentation/custom-tools)
- [External meeting authorization service](https://www.veeting.com/en/developer-documentation/external-meeting-authorization-service)
- [iFrame and Web Components](https://www.veeting.com/en/developer-documentation/iframe-and-web-components)
- [JavaScript APIs](https://www.veeting.com/en/developer-documentation/javascript-apis)
- [Query parameters](https://www.veeting.com/en/developer-documentation/query-parameters)
- [Veeting Blocks - Components](https://www.veeting.com/en/veeting-blocks/components)
- [Veeting Blocks - Introduction](https://www.veeting.com/en/veeting-blocks/introduction)
- [Veeting Blocks - JavaScript and Typescript APIs](https://www.veeting.com/en/veeting-blocks/apis)
- [Video display calculator](https://www.veeting.com/en/developer-documentation/video-display-calculator)
- [Web hooks](https://www.veeting.com/en/developer-documentation/web-hooks)

All of it in one file: https://www.veeting.com/llms-full.txt
