Meetily APIs
The full HTTP API surface: every route, its scope, request and response shapes, the error envelope, and the operation annotations - grounded in the running gateway.
The live manifest is canonical; this page is the readable map
The single source of truth for every route is the manifest the running gateway serves itself at GET /openapi.json (Read scope). It's OpenAPI 3.1, generated from the same code that serves the routes, so it can't drift from what's actually running. A committed copy, openapi.gen.json, ships alongside the app for offline tooling and MCP/SDK generators. This page mirrors that manifest in prose and can lag it by a release - when the two disagree, the manifest wins.
Base URL and transport
The gateway is a local HTTP listener exposed by a running Meetily Pro desktop app. It is Pro-gated and off by default - turn on the Automation API switch under Settings > Integrations first (see Enable & connect).
| Value | |
|---|---|
| Default address | http://127.0.0.1:8420 (loopback) |
| LAN address (when LAN access is on) | https://0.0.0.0:8421 (self-signed TLS) |
| Auth | Authorization: Bearer <token> on every route except /health and pairing |
| Content type | application/json for bodies, except import (application/octet-stream) |
The listener binds loopback only. It flips to 0.0.0.0 on the TLS port only when you explicitly enable LAN access; there is no hosted relay and no NAT traversal. Quit Meetily and the API stops serving immediately.
Scopes
A token carries one or more of exactly four scopes. There is no admin scope - the string admin parses to nothing and grants nothing.
| Scope | Grants |
|---|---|
read | Every GET, plus the SSE wait streams and /openapi.json. |
record | Recording control (start, stop, pause, resume). Also satisfies read. |
write | Creating, changing, or removing data (rename, summaries, jobs, config, webhooks). Also satisfies read. |
delete | Deleting a meeting. Granted separately; does not imply read or write, and is never handed to a paired device. |
read is the floor: a record- or write-scoped token can call read routes too. record, write, and delete each require their own literal scope - holding write does not grant record or delete.
Error envelope
Every non-2xx response has the same shape:
{"error": {"code": "...", "message": "..."}}code is a stable machine string; message is human-readable and may change. The codes the gateway emits:
| Code | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Malformed request, or references something invalid. |
invalid_request | 422 | The body or parameters failed validation. |
unauthorized | 401 | No token, or the token isn't recognized. |
forbidden | 403 | Valid token, but not allowed to do this. |
insufficient_scope | 403 | The token's scopes don't cover this call. |
not_prearmed | 403 | A remote caller tried to start recording without pre-arming - see Pairing & remote access. |
not_found | 404 | The resource doesn't exist. |
conflict | 409 | Conflicts with the resource's current state. Carries a specific sub-code (below). |
payload_too_large | 413 | The import body exceeds the per-file cap. |
too_many_requests | 429 | Rate- or budget-limited; carries a specific sub-code and often a Retry-After. |
license_required | 402 | The call needs a Pro license this install doesn't have. |
consumer_disabled | 403 | The calling key exists but its access is switched off. |
api_disabled | 403 | The Automation API itself is turned off. |
service_unavailable | 503 | A dependency needed for the call isn't available. |
internal | 500 | Something failed on Meetily's side. |
409 conflict and 429 too_many_requests narrow to a specific sub-code in code for common cases - for example import_not_pausable, not_retryable, not_terminal, summary_generating, recording_active, recording_in_progress, engine_busy (conflicts), and import_budget_full (budget). Match on the sub-code where one exists rather than the generic family.
Operation annotations
Every operation in the manifest carries four x-meetily-* annotations so tooling (including the MCP server) can reason about a call before making it:
x-meetily-read-only- the call only reads state.x-meetily-idempotent- safe to retry with the same result.x-meetily-destructive- deletes or otherwise can't be undone.x-meetily-required-scope- the scope a token needs to call it.
Routes
Every route below is prefixed by the base URL. The Scope column is the enforced floor; a few routes layer an ownership or per-event check on top, noted inline. SSE routes are curl -N streams that emit exactly one event, then close.
Health, identity, pairing
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /health | none | Liveness. No token. Always returns 200. |
GET | /v1/whoami | read | Returns WhoamiResponse - your token's scopes, license tier, and the server version. |
GET | /openapi.json | read | The canonical manifest. Read-scoped, not public. |
GET | /v1/search?q=&limit=&offset= | read | Full-text search across transcripts. Empty q → 400. limit clamps to 1..200. Returns SearchResponse. |
POST | /v1/pair/request | none | Bootstrap a token from another device. Body PairRequestBody. See Pairing & remote access. |
GET | /v1/pair/{id} | none | Poll a pairing request until it's approved or rejected. |
Pairing has no HTTP admin routes
Listing and revoking existing pairings, and minting an MCP client token, are in-process desktop actions, not HTTP routes - the former /v1/pairings* and /v1/mcp/client-token endpoints were removed. Only requesting and polling a new pairing are on the HTTP surface.
Meetings
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /v1/meetings?limit=&offset= | read | Paginated. limit clamps to 1..200. Returns MeetingListResponse (meetings, total, limit, offset). |
GET | /v1/meetings/{id} | read | One meeting - MeetingDto. |
PATCH | /v1/meetings/{id} | write | Rename. Body RenameMeetingRequest (title). |
DELETE | /v1/meetings/{id}?delete_files= | delete | Destructive, no undo. Needs the delete scope. delete_files (default false) also removes the recording file. |
GET | /v1/meetings/{id}/transcript | read | TranscriptResponse - segments with speaker, timestamps, and optional word timings. |
GET | /v1/meetings/{id}/summary | read | SummaryDto - status, result, and regeneration_failed. |
PUT | /v1/meetings/{id}/summary | write | Overwrite the stored summary outright. Body OverwriteSummaryRequest (text). No undo. |
POST | /v1/meetings/{id}/summary/regenerate | write | Kick off an LLM regeneration. Body RegenerateSummaryRequest (all optional). Returns 202 + RegenerateResponse with an operation_id and wait_url. |
GET | /v1/meetings/{id}/summary/operations/{operation_id} | read | Poll a regeneration operation's status. |
GET | /v1/meetings/{id}/summary/operations/{operation_id}/wait?timeout_ms= | read | SSE - resolves when that regeneration finishes. |
PUT | /v1/meetings/{id}/speaker-labels | write | Rename speakers. Body SpeakerLabelsRequest (labels object). |
GET | /v1/meetings/{id}/export?format= | read | format = json (default), md, or txt (aliases markdown, text). Returns ExportDto for json, otherwise the rendered document. |
Config
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /v1/config/{section} | read | Read one config section. Returns ConfigDto (section, config, withheld). |
PUT | /v1/config/{section} | write | Replace a section immediately - no dry-run. Arming auto-record additionally requires record and pre-arm. |
Valid sections: vad, diarization, speaker-identification, meeting-detection, global-shortcut, audio-player, custom-words, initial-prompt. Any other name → 404.
Jobs
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /v1/jobs | read | All jobs - JobListResponse. |
GET | /v1/jobs/{id} | read | One job - JobDto. Carries meeting_id once one is assigned. |
POST | /v1/jobs/diarization | write | Body SubmitDiarizationRequest (meeting_id, optional speaker_count). A rerun conflicting with a running job → 409. Returns JobIdResponse. |
POST | /v1/jobs/import | write | Import an external audio file. Body is the raw bytes (application/octet-stream); query filename (required), plus optional title, language, model, provider. Returns 202 + JobIdResponse. See Import a recording. |
POST | /v1/jobs/{id}/cancel | write | Cancel a running job. |
POST | /v1/jobs/{id}/pause | write | Pause. Import jobs can't pause → 409 import_not_pausable. |
POST | /v1/jobs/{id}/resume | write | Resume a paused job (not import). |
POST | /v1/jobs/{id}/retry | write | Retry a failed job, when is_retryable. |
GET | /v1/jobs/{id}/wait?timeout= | read | SSE - resolves when the job reaches a terminal state. |
Job state is `succeeded`, not `completed`
A JobDto.state moves through queued → running → succeeded (or failed, cancelled, plus the transient pausing/paused/resuming/cancelling). Terminal success is succeeded. The webhook event named job.completed is a different thing from the job state - don't poll for a completed state, it never appears.
Devices and models
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /v1/devices | read | Audio input/output devices - DeviceListResponse. |
GET | /v1/models | read | Transcription/summary models and their status - ModelListResponse. |
Recording
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /v1/recording | read | Current recording state - RecordingStateDto (state, active_meeting_id, last_error). |
POST | /v1/recording/start | record | Start capture. Body StartRecordingRequest - all fields optional. A remote (non-loopback) caller is refused 403 not_prearmed unless pre-armed. |
POST | /v1/recording/stop?session= | record | Stop and finalize. |
POST | /v1/recording/pause | record | Pause capture. |
POST | /v1/recording/resume | record | Resume capture. |
GET | /v1/recording/wait?until=&timeout= | read | SSE - until (required) is started or stopped. Resolves on that transition. Note this is read, not record. |
`consent_attested` is a real field
StartRecordingRequest.consent_attested is optional. Omitting it, or sending true, starts recording. Sending an explicit false is rejected with 400 ("consent_attested must be true to start a recording"). It captures real microphone and/or system audio the instant it returns - run it deliberately.
Webhooks
Registration, delivery mechanics, the HMAC signature, the SSRF allowlist, and the approval_state gate are all covered in depth in Webhooks & SSE; this is the route surface.
| Method | Path | Scope | Notes |
|---|---|---|---|
POST | /v1/webhooks | read floor + per-event scope | Register. Body CreateWebhookRequest (url, events[], optional delivery_mode). Returns 201 with the one-time hmac_secret. Each event you subscribe to is scope-gated to what your token can read. |
GET | /v1/webhooks | read | Your own webhooks. ?all=true needs delete + a first-party token. |
GET | /v1/webhooks/{id} | read | One webhook - includes its approval_state. Owner-only. |
DELETE | /v1/webhooks/{id} | read floor | Delete one you own. Does not need the delete scope (that scope is for meetings). Owner-only. |
GET | /v1/webhooks/{id}/deliveries | read | Delivery log (most recent 100). Owner-only. |
POST | /v1/webhooks/{id}/test | read floor | Send a synthetic delivery to confirm your receiver + signature check. Owner- and egress-consent-gated. Returns 202. |
The 17 subscribable event names, the thin notification-only envelope, and which events are dormant are in the Event catalogue. Note that meeting.* and transcription.completed are not subscribable and are rejected at registration with 400.
Key schemas
Request and response bodies, from the manifest. * marks a required field; ? marks nullable/optional.
Requests
| Schema | Fields |
|---|---|
RenameMeetingRequest | title* |
OverwriteSummaryRequest | text* |
RegenerateSummaryRequest | model?, model_name?, template_id?, language? (all optional; omitted fields fall back to the app's stored config) |
SpeakerLabelsRequest | labels* (object, e.g. {"Speaker 1":"Alice"}) |
SubmitDiarizationRequest | meeting_id*, speaker_count? (omit for auto) |
StartRecordingRequest | meeting_name?, mic_device_name?, system_device_name?, consent_attested? |
CreateWebhookRequest | url*, events* (non-empty), delivery_mode? (at-least-once default, or at-most-once) |
PairRequestBody | client_name*, scopes* |
Responses
| Schema | Fields |
|---|---|
WhoamiResponse | token_id*, scopes*, license_tier*, server_version* |
MeetingListResponse | meetings* (MeetingDto[]), total*, limit*, offset* |
MeetingDto | id*, title*, created_at*, updated_at* |
TranscriptResponse | meeting_id*, title*, segments* (TranscriptSegmentDto[]) |
SummaryDto | meeting_id*, status*, result?, regeneration_failed, error?, updated_at* |
RegenerateResponse | operation_id*, wait_url*, status_url*, state*, meeting_id*, run_id?, ... |
JobDto | id*, kind*, state*, stage*, progress*, meeting_id?, is_retryable*, error?, created_at*, ... |
JobIdResponse | job_id* |
RecordingStateDto | state*, active_meeting_id?, last_error? |
SearchResponse | results* (SearchResultDto[] - meeting_id, title, match_context, timestamp) |
ConfigDto | section*, config*, withheld* |
DeviceListResponse / ModelListResponse | devices* / models* |
ExportDto | meeting*, transcript*, summary? |
Is there an SDK?
No. There is no published or maintained SDK. The supported clients are the meetily-pro CLI and the raw HTTP API documented here; for an MCP-compatible assistant, use the MCP server.
An unsupported, vendored Python helper exists in the separate meetily-workflows examples repo. It isn't part of this product and isn't maintained as one - treat it as example code, not a dependency.
Last updated on
