> For an agent: the map of this documentation is at https://escouade.io/en/docs/llms.txt; every page exists in Markdown, at the same address with `.md` appended.

# Meet for developers

> Meet has a read-only REST API, signed webhooks and an MCP server. Keys, webhooks and connected assistants are managed in Meet's space, account menu "Développeurs" (`/developpeurs`).

The REST API, the keys and the webhooks are **read-only**: nothing there creates, changes or deletes a meeting, minutes or an action. A key only reads the meetings its owner owns, outside the bin; never those other members share with their teams.

## API keys

A key is created in Développeurs › Clés, with a name and at least one permission. Its value is shown **once only**, right after creation. It has the form `rmk_` followed by an eight-character public identifier, a `_` and a secret; only its fingerprint is kept. It can be revoked at any time, for good. An account has at most 20 active keys.

| Permission | Opens |
|---|---|
| `meetings:read` | The list and details of meetings: title, date, status, origin, calendar context. |
| `reports:read` | Minutes: summary, key points, decisions, open questions, chapters. |
| `actions:read` | The actions in the minutes: owner, deadline, "done" status. |
| `transcripts:read` | Transcriptions, with a permission of their own. |
| `usage:read` | The month's time, through the MCP server (`get_usage` tool). |

## The REST API

Base address: `https://meet.escouade.io/v1`. Authentication: `Authorization: Bearer rmk_…`. Responses in JSON, dates in RFC 3339 (UTC). Any method other than `GET` receives `405`. The API sends no CORS headers: it is called from a server, never from a web page, where a key must not live.

| Resource | Permission | Returns |
|---|---|---|
| `GET /v1/meetings` | `meetings:read` | `{"data": [meeting…], "next_cursor": "…" or null}` |
| `GET /v1/meetings/{id}` | `meetings:read` | `{"data": meeting}` |
| `GET /v1/meetings/{id}/report` | `reports:read` | `{"data": minutes}`, without the actions |
| `GET /v1/meetings/{id}/actions` | `actions:read` | `{"data": [action…]}` |
| `GET /v1/meetings/{id}/transcript` | `transcripts:read` | `{"data": {"meeting_id", "segments": […]}}` |

```sh
curl -s https://meet.escouade.io/v1/meetings?limit=5 \
  -H 'Authorization: Bearer rmk_…'
```

### List and pagination

`GET /v1/meetings` returns the most recent meetings first. Parameters: `limit` (1 to 100, 20 by default), `cursor` (the `next_cursor` value of the previous page, as is), `status` (a status below). `next_cursor` is `null` on the last page.

### A meeting

| Field | Content |
|---|---|
| `id` | The meeting's identifier. |
| `title` | Title. |
| `status` | Public status (below). |
| `starts_at` | Start; for a phone recording, when it was sent. |
| `scheduled_end_at` | Scheduled end, for a calendar meeting; otherwise `null`. |
| `origin` | `calendar` (connected calendar), `link` (pasted link), `phone` (recorder), `desktop`, `manual` (meeting noted in Meet). |
| `platform` | `meet`, `teams`, `zoom`, `webex`, `goto`, `in_person`, `desktop`, `other`. |
| `organizer_email` | Organiser known from the calendar, otherwise `null`. |
| `context` | Calendar description as plain text (1,200 characters at most), otherwise `null`. |
| `has_transcript`, `has_report` | A transcription, minutes exist. |
| `updated_at` | Last change. |
| `participants` | `{"email", "name", "response", "organizer"}`, organiser first; `response` among `accepted`, `tentative`, `declined`, `pending`, `unknown`. |

Never: the video call link, a file path, an internal technical identifier.

### Statuses

| `status` | Meaning |
|---|---|
| `planned` | Scheduled. |
| `joining` | The bot is joining the meeting or waiting in the waiting room. |
| `recording` | Recording in progress. |
| `processing` | Transcription and minutes being prepared. |
| `transcript_ready` | Finished with a transcription, without minutes. |
| `report_ready` | Finished with minutes. |
| `completed` | Finished without transcription or minutes. |
| `failed` | Processing did not succeed. |
| `missed` | Missed. |
| `unsupported` | Platform not supported. |

### Minutes, actions, transcription

- **Minutes**: `meeting_id`, `template`, `summary`, `key_points`, `decisions`, `open_questions` (each `{"text", "source_seconds"}`), `chapters` (`{"title", "summary", "source_seconds"}`). No minutes yet: `404 not_ready`.
- **Action**: `id` (stable key), `title`, `assignee`, `due` (free text as spoken, may be `null`), `completed`, `source_seconds`.
- **Transcription**: `segments` of `{"start", "end", "speaker", "text"}`, in seconds; `speaker` is `null` when the speaker is not known.

### Errors and limits

Error body: `{"error": {"code": "…", "message": "…"}}`; for a `403`, `required_scope` as well.

| HTTP | `code` | When |
|---|---|---|
| 400 | `invalid_parameter` | `limit` or `status` outside the allowed values. |
| 400 | `invalid_cursor` | Unreadable cursor. |
| 401 | `unauthorized` | Key missing, unknown or revoked. |
| 403 | `insufficient_scope` | Missing permission. |
| 404 | `not_found` | Meeting that does not exist, belongs to another account or is in the bin. |
| 404 | `not_ready` | No minutes or transcription yet. |
| 405 | | Method other than `GET`. |
| 429 | `rate_limited` | More than 120 calls per minute for the key, or from the same IP address for the API and MCP combined. `Retry-After` header. |
| 500 | `internal_error` | Service error. |

## Webhooks

Destinations are managed in Développeurs › Webhooks: an **HTTPS** address reachable on the Internet, the chosen events, at most 10 destinations per account.

| Event | Sent when |
|---|---|
| `meeting.transcript_ready` | A transcription is saved (it may be empty if nobody spoke). |
| `meeting.report_ready` | The minutes are saved. |
| `meeting.processing_failed` | The recording or the transcription did not succeed. |

The body carries only the essentials; the title and content are then read through the API, with a key:

```json
{"id": "evt_…", "type": "meeting.report_ready", "created_at": "2026-10-20T09:41:07Z",
 "data": {"meeting_id": "…", "status": "report_ready"}}
```

### Verifying the signature

The headers follow the [Standard Webhooks](https://www.standardwebhooks.com/) convention:

- `webhook-id`: the event's identifier, **the same on every retry**;
- `webhook-timestamp`: the moment, in Unix seconds;
- `webhook-signature`: `v1,` followed by the HMAC-SHA256, in base64, of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the `whsec_…` secret without its prefix, decoded from base64.

Verify in constant time, refuse a timestamp more than 5 minutes off, ignore a `webhook-id` already processed. The secret is shown once, when the destination is created.

```python
import base64, hashlib, hmac, time

def verify(secret, headers, body):
    key = base64.b64decode(secret.removeprefix("whsec_"))
    message = "%s.%s.%s" % (headers["webhook-id"], headers["webhook-timestamp"], body)
    expected = base64.b64encode(hmac.new(key, message.encode(), hashlib.sha256).digest()).decode()
    if abs(time.time() - int(headers["webhook-timestamp"])) > 300:
        return False
    return any(hmac.compare_digest("v1," + expected, s) for s in headers["webhook-signature"].split())
```

### Delivery

Your endpoint replies `2xx` within 10 seconds. Otherwise, a retry goes out after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h then 6 h: **8 attempts at most**, then the event is marked abandoned. A delivered or abandoned event can be resent from the page, with the same identifier. No redirect is followed. A paused destination accumulates its events, which go out when it resumes.

## The MCP server

Address: **`https://meet.escouade.io/mcp`**, over Streamable HTTP, without sessions. An assistant connects through OAuth, with the address alone; a program, with an `rmk_…` key in `Authorization: Bearer`.

Twenty-one tools: eleven **read**, five **prepare** an action and five are reserved for the assistant's views.

| Read tool | Permission | What it returns |
|---|---|---|
| `search_meetings` | `meetings:read` | The meetings matching a search. |
| `get_meeting` | `meetings:read` | A meeting. |
| `get_report` | `reports:read` | Its minutes. |
| `get_action_items` | `actions:read` | Its actions. |
| `get_transcript` | `transcripts:read` | Its transcription. |
| `get_today` | `meetings:read` | A day's meetings, and the open actions with `actions:read`. |
| `get_robot_status` | `meetings:read` | Where the bot stands in a meeting. |
| `list_action_items` | `actions:read` | The actions of recent minutes, filtered by owner or status. |
| `search_passages` | `transcripts:read` | The passages matching a search, by meaning or by exact word. |
| `ask_meetings` | `reports:read` and `transcripts:read` | The answer to a question, with its sources; `found` is `false` when the information is missing. |
| `get_usage` | `usage:read` | The month's time: included, used, remaining. |

### Prepare, then confirm

An assistant connected through OAuth can **prepare** an action; it is only carried out once **confirmed** by the person, in the assistant's view or on a Meet page. Never through an API key, never through the REST API.

| Tool | Permission | What confirmation will do |
|---|---|---|
| `send_robot` | `meetings:write` | Sends the bot into the video call, now. |
| `stop_robot` | `meetings:write` | Removes the bot from the meeting. |
| `create_meeting` | `meetings:write` | Notes a meeting in Meet, with no video link and no calendar invitation. |
| `update_action_item` | `actions:write` | Ticks or unticks an action in the minutes. |
| `share_report` | `reports:share` | Sends each recipient a viewing link to the minutes, valid for 7 days. |

A prepared action waits for 10 minutes. What leaves Meet with no way back (an email sent, the bot sent through a pasted link) can only be confirmed on the Meet page. No deletion, no payment, no sending of a transcription or recording by this route.

### OAuth authorisation

Meet follows the authorisation part of the MCP specification: OAuth 2.1, public clients, mandatory PKCE S256, dynamic registration and client metadata documents. The consent page lists permissions in two groups, **Read** and **Act**; only the four meeting reads are ticked by default, nothing that acts. The access token lasts an hour and is renewed; Développeurs › Serveur MCP lists the connected assistants, with "Revoke".

### Connecting the assistant

- **Claude**: Settings › Connectors › "Add custom connector", name "Escouade Meet", address `https://meet.escouade.io/mcp`; "Connect", then your Escouade account, the permissions, "Allow".
- **ChatGPT**: in developer mode, a connector with the same address and OAuth authentication.
- **Claude Code**:

```sh
claude mcp add --transport http escouade-meet https://meet.escouade.io/mcp
```

A connector already connected to `https://robotmeet.fr/mcp` keeps working; moving to the new address means connecting it again, then revoking the old one.
