---
title: Meetings
description: List, read, create, update and delete your organization's Meetings.
product: gps
section: api
order: 15
---

# Meetings

| Method | Path | Scope | Webhook |
|--------|------|-------|---------|
| `GET` | [`/meetings`](#list-meetings) | `meetings:read` | |
| `GET` | [`/meetings/{id}`](#get-a-meeting) | `meetings:read` | |
| `POST` | [`/meetings`](#create-a-meeting) | `meetings:write` | None |
| `PATCH` | [`/meetings/{id}`](#update-a-meeting) | `meetings:write` | None |
| `DELETE` | [`/meetings/{id}`](#delete-a-meeting) | `meetings:write` | None |

Meeting changes don't fire webhooks. Related: [Meeting comments](comments.md).

Archived Meetings aren't returned: lists leave them out and `GET`, `PATCH` and `DELETE` return `404` for them.

The JavaScript examples assume a `headers` object holding your key:

```js
const headers = {
  'X-API-Key': process.env.GPS_API_KEY_ID,
  'X-API-Secret': process.env.GPS_API_SECRET,
};
```

## The Meeting object

| Field | Type | Notes |
|-------|------|-------|
| `id` | string | Meeting ID. |
| `title` | string | |
| `description` | string or null | |
| `status` | string or null | `scheduled`, `in_progress`, `paused`, `completed`, `cancelled` or `resolved_async` (settled through check-ins, so never held). |
| `meetingType` | string or null | Free text, for example `Weekly Check-in`. |
| `scheduledDate` | string | Scheduled start, ISO 8601 timestamp. |
| `duration` | integer or null | Planned length in minutes. |
| `actualStartTime` | string or null | When it started. |
| `actualEndTime` | string or null | When it ended. |
| `actualDuration` | integer or null | Actual length in minutes. |
| `location` | string or null | |
| `isVirtual` | boolean or null | |
| `meetingUrl` | string or null | Video call link. |
| `attendees` | string[] | Member IDs. |
| `externalAttendees` | string[] | Email addresses of people outside the organization. |
| `notes` | string or null | |
| `meetingSummary` | string or null | Summary written after the Meeting. |
| `keyDecisions` | string[] | The text of each decision recorded in the Meeting. |
| `participantCount` | integer | How many people took part. |
| `recordingUrl` | string or null | |
| `createdBy` | string or null | Member ID of the creator. |
| `organizationId` | string or null | |
| `createdAt` | string | ISO 8601 timestamp. |
| `updatedAt` | string | ISO 8601 timestamp. |

## List Meetings

`GET /meetings`

Order isn't guaranteed; sort by `scheduledDate` on your side.

**Scope:** `meetings:read`

**Query parameters**

| Parameter | Type | Notes |
|-----------|------|-------|
| `status` | string | One of `scheduled`, `in_progress`, `paused`, `completed`, `cancelled`, `resolved_async`. |
| `limit` | integer | 1–200, default 50. |
| `offset` | integer | Default 0. |

```bash
curl "https://plan.empiraa.app/api/v1/meetings?status=completed" \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/meetings?status=completed', { headers });
const { data: meetings, meta } = await res.json();
```

**Response `200`**

```json
{
  "data": [
    {
      "id": "7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c",
      "title": "Monthly strategy review",
      "description": "Review revenue Objectives and agree next steps.",
      "status": "completed",
      "meetingType": "Strategic Planning",
      "scheduledDate": "2026-11-05T22:00:00.000Z",
      "duration": 60,
      "actualStartTime": "2026-11-05T22:02:11.000Z",
      "actualEndTime": "2026-11-05T22:58:40.000Z",
      "actualDuration": 56,
      "location": null,
      "isVirtual": true,
      "meetingUrl": "https://meet.example.com/abc-defg-hij",
      "attendees": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70", "6e8a0c2e-4f6b-4d8f-a0b1-3c4d5e6f7a81"],
      "externalAttendees": ["advisor@example.com"],
      "notes": "Pricing review moved to December.",
      "meetingSummary": "Revenue is on track; churn needs attention.",
      "keyDecisions": ["Launch the annual plan discount on 1 December"],
      "participantCount": 3,
      "recordingUrl": null,
      "createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
      "organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
      "createdAt": "2026-10-29T01:00:00.000Z",
      "updatedAt": "2026-11-05T23:00:12.000Z"
    }
  ],
  "meta": { "total": 1, "limit": 50, "offset": 0 }
}
```

**Errors:** `400 INVALID_STATUS_QUERY` for an unknown `status`; `422 VALIDATION_ERROR` for a bad `limit` or `offset`.

## Get a Meeting

`GET /meetings/{id}`

**Scope:** `meetings:read`

| Path parameter | Notes |
|----------------|-------|
| `id` | Meeting ID. |

```bash
curl https://plan.empiraa.app/api/v1/meetings/7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/meetings/7a9c1e3f-5b6d-4e8a-8b9c-4d5e6f7a8b9c', { headers });
const { data: meeting } = await res.json();
```

**Response `200`**: `{ "data": <Meeting> }`, with the same fields as the item in [List Meetings](#list-meetings).

**Errors:** `404 NOT_FOUND` if the Meeting doesn't exist, is in another organization, or is archived.

## Create a Meeting

`POST /meetings`

**Scope:** `meetings:write` · **Webhook:** none.

**Request body**

| Field | Type | Required | Allowed values | Notes |
|-------|------|----------|----------------|-------|
| `title` | string | Yes | At least 1 character after trimming | |
| `scheduledDate` | string | Yes | A valid ISO 8601 date-time | Scheduled start. Include a time zone offset. |
| `description` | string or null | No | | |
| `duration` | integer or null | No | | Minutes. Default 60. |
| `attendees` | string[] or null | No | Member IDs | Not checked against your member list, so send IDs from [`GET /users`](members.md). |
| `externalAttendees` | string[] or null | No | Email addresses | |
| `meetingType` | string or null | No | | |
| `location` | string or null | No | | |
| `isVirtual` | boolean or null | No | | Default `true`. |
| `meetingUrl` | string or null | No | | |
| `status` | string | No | `scheduled`, `in_progress`, `paused`, `completed`, `cancelled`, `resolved_async` | Default `scheduled`. |
| `notes` | string or null | No | | |
| `onBehalfOf` | string | No | Active member ID | Who the Meeting is created by. See [Attribution](../conventions.md#attribution-with-onbehalfof). |

```bash
curl -X POST https://plan.empiraa.app/api/v1/meetings \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Monthly strategy review",
    "scheduledDate": "2026-12-03T09:00:00+11:00",
    "duration": 60,
    "meetingType": "Strategic Planning",
    "attendees": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
    "meetingUrl": "https://meet.example.com/abc-defg-hij"
  }'
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/meetings', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    title: 'Monthly strategy review',
    scheduledDate: '2026-12-03T09:00:00+11:00',
    duration: 60,
    meetingType: 'Strategic Planning',
    attendees: ['5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70'],
    meetingUrl: 'https://meet.example.com/abc-defg-hij',
  }),
});
const { data: meeting } = await res.json();
```

**Response `201`**

```json
{
  "data": {
    "id": "8b0d2f4a-6c7e-4f9b-9c0d-5e6f7a8b9c0d",
    "title": "Monthly strategy review",
    "description": null,
    "status": "scheduled",
    "meetingType": "Strategic Planning",
    "scheduledDate": "2026-12-02T22:00:00.000Z",
    "duration": 60,
    "actualStartTime": null,
    "actualEndTime": null,
    "actualDuration": null,
    "location": null,
    "isVirtual": true,
    "meetingUrl": "https://meet.example.com/abc-defg-hij",
    "attendees": ["5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70"],
    "externalAttendees": [],
    "notes": null,
    "meetingSummary": null,
    "keyDecisions": [],
    "participantCount": 0,
    "recordingUrl": null,
    "createdBy": "5d7f9b1d-3e5a-4c7e-9f1a-2b3c4d5e6f70",
    "organizationId": "9b2f4e1a-6c3d-4e8f-a1b2-c3d4e5f6a7b8",
    "createdAt": "2026-11-12T03:18:00.511Z",
    "updatedAt": "2026-11-12T03:18:00.511Z"
  }
}
```

**Errors**

| Status | `code` | When |
|--------|--------|------|
| 422 | `VALIDATION_ERROR` | Missing `title` or `scheduledDate`, `scheduledDate` isn't a valid date, or a value outside the allowed list. |
| 422 | `INVALID_USERS` | `onBehalfOf` isn't an active member. |
| 422 | `NO_KEY_OWNER` | No `onBehalfOf`, and the key's creator is no longer a member. |

## Update a Meeting

`PATCH /meetings/{id}`

Changes only the fields you send. `attendees` and `externalAttendees` replace the whole list.

**Scope:** `meetings:write` · **Webhook:** none.

| Path parameter | Notes |
|----------------|-------|
| `id` | Meeting ID. |

**Request body:** any field from [Create a Meeting](#create-a-meeting) except `onBehalfOf`, with the same rules. Send at least one field.

```bash
curl -X PATCH https://plan.empiraa.app/api/v1/meetings/8b0d2f4a-6c7e-4f9b-9c0d-5e6f7a8b9c0d \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "scheduledDate": "2026-12-04T09:00:00+11:00", "location": "Boardroom", "isVirtual": false }'
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/meetings/8b0d2f4a-6c7e-4f9b-9c0d-5e6f7a8b9c0d', {
  method: 'PATCH',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ scheduledDate: '2026-12-04T09:00:00+11:00', location: 'Boardroom', isVirtual: false }),
});
const { data: meeting } = await res.json();
```

**Response `200`**: `{ "data": <Meeting> }` with the changes applied, for example `"scheduledDate": "2026-12-03T22:00:00.000Z"`, `"location": "Boardroom"` and `"isVirtual": false`. Every other field is as in [Create a Meeting](#create-a-meeting).

**Errors:** `404 NOT_FOUND` if the Meeting doesn't exist, is in another organization or is archived; `422 VALIDATION_ERROR` for an invalid value.

## Delete a Meeting

`DELETE /meetings/{id}`

Permanently deletes the Meeting.

**Scope:** `meetings:write` · **Webhook:** none.

| Path parameter | Notes |
|----------------|-------|
| `id` | Meeting ID. |

```bash
curl -X DELETE https://plan.empiraa.app/api/v1/meetings/8b0d2f4a-6c7e-4f9b-9c0d-5e6f7a8b9c0d \
  -H "X-API-Key: $GPS_API_KEY_ID" \
  -H "X-API-Secret: $GPS_API_SECRET"
```

```js
const res = await fetch('https://plan.empiraa.app/api/v1/meetings/8b0d2f4a-6c7e-4f9b-9c0d-5e6f7a8b9c0d', {
  method: 'DELETE',
  headers,
});
console.log(res.status); // 204
```

**Response `204`**: no body.

**Errors:** `404 NOT_FOUND` if the Meeting doesn't exist, is in another organization, or is archived.
