API Reference
External integrations use the Meetric partner REST API under /rest with mt_* bearer tokens.
Getting Started#
REST Contract
OpenAPI schema is the source of truth
Scoped Tokens
Every endpoint is controlled by token scopes
Rate Limited
Per-token limits and daily conversation quotas
Base URL and OpenAPI#
REST API base URL: http://127.0.0.1:37521/api/v1/restOpenAPI spec: http://127.0.0.1:37521/api/v1/rest/openapi.jsonNote
Authentication#
curl -X GET "http://127.0.0.1:37521/api/v1/rest/conversations?limit=25" \
-H "Authorization: Bearer mt_your_api_token"Generate tokens in Settings -> REST API -> API Tokens. Tokens are shown once and should be stored securely. Webhook destinations are configured separately under Settings -> REST API -> Webhook subscriptions — see the Webhooks section below.
Warning
/rest endpoints only. Internal JWT endpoints under /api/v1 are not part of this partner API contract.Endpoint Contract#
Current partner API endpoints and required scopes, aligned with the REST OpenAPI contract.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /rest/account | none | Get account and branding context |
| GET | /rest/users | users | List active users in account |
| GET | /rest/teams | teams | List teams with members |
| GET | /rest/departments | departments | List enabled departments |
| GET | /rest/conversations | conversations | List conversations (30-day window, cursor pagination) |
| GET | /rest/conversations/:id | conversations | Get conversation detail (optional transcript/insights includes) |
| GET | /rest/conversations/:id/transcript | transcripts | Get transcript payload for one conversation |
| GET | /rest/conversations/:id/insights | insights | Get insights/topics for one conversation |
| GET | /rest/conversations/:id/recording-url | recordings + include_recordings=true | Get recording URL if available |
| GET | /rest/conversations/:id/summary | conversations | Get stored summary text for one conversation |
| POST | /rest/notetaker/request | notetaker | Request notetaker join for supported meeting URL |
| GET | /rest/webhooks/deliveries | none | List recent webhook deliveries (last 7 days) |
Note
GET /rest/conversations is limited to the last 30 days and max limit=25 per page. Use pagination.next_cursor for additional pages.Recordings dual-gate
GET /rest/conversations/:id/recording-url requires both:
- the
recordingsscope on the token, and - the
include_recordings = truesetting on the token.
Missing scope returns 403 with code: missing_scope. Scope granted but token flag off returns 403 with code: recordings_not_allowed. The Settings UI couples both controls so admins cannot create an invalid combination.
Transcript Endpoint#
GET /rest/conversations/:id/transcript#
curl -X GET "http://127.0.0.1:37521/api/v1/rest/conversations/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transcript" \
-H "Authorization: Bearer mt_your_api_token"{
"conversation_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"transcript": {
"sentences": [
{
"speaker_name": "Erik Lindstrom",
"start": 0.84,
"end": 3.14,
"text": "Thanks for joining today."
}
]
},
"_meta": {
"token_prefix": "mt_ab12cd34",
"timestamp": "2026-06-03T09:42:00.000Z"
}
}Tip
transcript_data. Some sources use sentences, others use messages. You can also use GET /rest/conversations/:id?include_transcript=true to embed transcript in conversation detail.Webhooks#
Webhook deliveries fire automatically when a conversation is fully processed (transcript, insights, and summary are ready). Webhook destinations are configured as subscriptions under Settings -> REST API -> Webhook subscriptions. Each subscription has its own URL, signing secret, scopes, optional department filter, and include_recordings flag. The signing secret is shown once on create or rotate.
One account can have many subscriptions. Each enabled subscription receives its own delivery for the same conversation event — subscription_id identifies which one in both the payload and the /rest/webhooks/deliveries audit endpoint.
Delivery headers#
X-Webhook-Signature: sha256=<hmac_sha256_of_raw_body>
X-Webhook-Event: conversation.completed
X-Webhook-Delivery-Id: <delivery_uuid>
X-Webhook-Timestamp: <unix_timestamp_seconds>X-Webhook-Delivery-Id is stable across retries for the same delivery. Use it for idempotency. X-Webhook-Timestamp is refreshed on every attempt (including retries) so replay protection works on retries too.
Verification and replay protection#
- Read the raw request body before any JSON parsing.
- Compute
expected = HMAC_SHA256(subscription_webhook_secret, raw_body)using the signing secret of the subscription that received the delivery (matched via_meta.subscription_id). - Compare
expectedagainst the hex value inX-Webhook-Signatureusing a constant-time comparison (for examplehmac.compare_digestin Python orcrypto.timingSafeEqualin Node.js). - Reject the request when
|now_unix_seconds - X-Webhook-Timestamp| > 300(5 minute replay window). We never deliver payloads older than this; anything past 5 minutes is treated as a replay. - Treat repeated
X-Webhook-Delivery-Idvalues as the same delivery and skip re-processing.
import crypto from "node:crypto";
function verifyMeetricWebhook(req, secret) {
const rawBody = req.rawBody; // Buffer or string captured BEFORE JSON parsing
const sigHeader = req.headers["x-webhook-signature"] || "";
const tsHeader = Number(req.headers["x-webhook-timestamp"] || "0");
// 1) Replay window: 5 minutes
if (Math.abs(Math.floor(Date.now() / 1000) - tsHeader) > 300) {
return { ok: false, reason: "replay_window_exceeded" };
}
// 2) HMAC-SHA256 of the raw body
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// 3) Constant-time compare
const sigBuf = Buffer.from(sigHeader);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return { ok: false, reason: "bad_signature" };
}
return { ok: true };
}Retries and idempotency#
Meetric retries failed deliveries with exponential backoff using the same X-Webhook-Delivery-Id. Schedule:
- Attempt 1: immediate
- Attempt 2: ~1 minute later
- Attempt 3: ~5 minutes later
- Attempt 4: ~30 minutes later
- Attempt 5: ~2 hours later
- Attempt 6: ~12 hours later (final)
We consider a delivery successful when your endpoint returns a 2xx status code within 10 seconds. Use GET /rest/webhooks/deliveries to inspect recent attempts (last 7 days) across all subscriptions for your account; each row includes subscription_id, and you can filter the list by ?subscription_id=<uuid> for a per-destination view.
conversation.completed payload#
{
"event": "conversation.completed",
"timestamp": "2026-06-03T09:45:00.000Z",
"data": {
"conversation": {
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"title": "Discovery call - Acme",
"company": "Acme",
"date": "2026-06-03T09:30:00.000Z",
"duration": 1800,
"medium": "call",
"sentiment": "positive",
"conversation_type": "sales",
"summary": "Customer requested pricing follow-up.",
"department": { "id": "11111111-0000-0000-0000-000000000001", "name": "B2B Sales" },
"user": { "id": "67c869eb-30b1-4eb3-bd2c-be8c912a9827", "name": "Erik Lindstrom", "email": "[email protected]" },
"participants": [
{ "name": "Jane Doe", "email": "[email protected]", "role": "customer" }
],
"transcript": { "sentences": [] },
"insights": [],
"recording_url": "https://example.com/recording"
}
},
"_meta": {
"subscription_id": "11111111-2222-3333-4444-555555555555",
"delivery_id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff",
"token_prefix": "mt_ab12cd34"
}
}Scope-conditional fields
transcriptrequires thetranscriptsscope.insightsrequires theinsightsscope.recording_urlrequires therecordingsscope andinclude_recordings = trueon the subscription.
_meta fields#
subscription_id— canonical correlation key. Always present. Matches the subscription you configured in Settings -> REST API -> Webhook subscriptions.delivery_id— same value as theX-Webhook-Delivery-Idheader. Stable across retries.token_prefix— DEPRECATED. Retained for one minor version so existing integrations that read_meta.token_prefixkeep working. Set to the prefix of a same-account API token that has the same webhook URL when one exists, otherwisenull. Usesubscription_idinstead. This field is removed in a future release.
Limits and Errors#
Rate Limits#
{
"requests_per_minute": 60,
"daily_conversation_quota": 1000
}Limits are configured per token. Daily quota counts unique conversations accessed per day. When the per-minute limit is exceeded, the response includes a Retry-After header (seconds) — honor it instead of retrying immediately.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded",
"code": "rate_limit_exceeded",
"details": { "retry_after_seconds": 60 }
}Error Envelope#
Every 4xx response on /rest/* uses the same envelope. Branch on the stable code field; treat message as human-readable text that may change between releases.
{
"statusCode": 403,
"error": "Forbidden",
"message": "Token is missing required scope: transcripts",
"code": "missing_scope",
"details": { "required_scope": "transcripts" }
}Note
Envelope fields:
code— stable, machine-readable identifier. Safe to branch on. We never rename or repurpose a published code.message— human-readable explanation. May change between releases. Do not parse.details— optional object carrying error-specific context (for examplerequired_scope,retry_after_seconds,daily_quota).
HTTP Status Codes#
- 200 - request succeeded
- 400 - invalid parameters or body
- 401 - token missing, invalid, inactive, revoked, or expired
- 403 - scope missing, recordings not enabled, or daily quota reached
- 404 - resource not found in this account / not visible to this token
- 429 - per-minute rate limit exceeded (honor
Retry-After) - 500 - internal server error
Stable Error Codes#
The code field uses these stable identifiers. Use them for retry/branching logic instead of matching on the message.
| Status | code | When |
|---|---|---|
| 401 | missing_api_token | No Authorization header on the request |
| 401 | invalid_api_token | Token does not match any record |
| 401 | token_inactive | Token exists but is disabled |
| 401 | token_revoked | Token has been revoked |
| 401 | token_expired | Token has passed its expires_at |
| 403 | missing_scope | Token is missing the scope required by this endpoint |
| 403 | recordings_not_allowed | Token has 'recordings' scope but include_recordings=false |
| 403 | quota_exceeded | Daily unique-conversation quota reached for this token |
| 404 | conversation_not_found | Conversation does not exist or is not visible to this token |
| 429 | rate_limit_exceeded | Per-minute request rate limit hit (honor Retry-After) |
Support#
Note