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:37837/api/v1/restOpenAPI spec: http://127.0.0.1:37837/api/v1/rest/openapi.jsonAuthentication#
curl -X GET "http://127.0.0.1:37837/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. You can later edit a token's name, description and scopes without changing the token string. Webhook destinations are configured separately under Settings -> REST API -> Webhook subscriptions — see the Webhooks section below.
/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) |
GET /rest/conversations is limited to the last 30 days and max limit=25 per page. Use pagination.next_cursor for additional pages.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:37837/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"
}
}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",
"content_ready": {
"transcript": true,
"summary": true,
"insights": false,
"recording": true
}
}
}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.content_ready— object of booleans describing which conversation parts were present when this delivery was built. Use it to distinguish "empty / not applicable" from "not ready yet" instead of re-fetching over REST.transcript/summary— always true on a delivered conversation.completed event. The webhook is not enqueued until both are ready.insights/recording— may be false on a delivered event. Insights and recordings are best-effort and do not block delivery — false means that part was not present (or not in scope), not that the event was sent too early.
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.
Test deliveries#
Pressing Test on a webhook subscription sends a test delivery. It carries a sample conversation in the same shape as conversation.completed, filtered by the subscription scopes, so you can test your integration before you have any real conversations. Test deliveries are listed in the delivery log like any other delivery. On test rows in the delivery list, resource_id is the delivery id, not a conversation id.
{
"event": "webhook.test",
"data": {
"message": "This is a test webhook delivery",
"subscription_name": "<subscription name>",
"conversation": {
"id": "00000000-0000-4000-8000-000000000000",
"...": "same fields as conversation.completed"
}
},
"_meta": { "subscription_id": "<uuid>", "delivery_id": "<uuid>", "test": true }
}- The sample conversation always has the id
00000000-0000-4000-8000-000000000000and never resolves to a real conversation. - Always check
event/X-Webhook-Eventbefore processing data.conversation. Only conversation.completed describes a real conversation. - Requesting a notetaker returns conversation_id as null, because the conversation is created only after the meeting ends and is processed. Use the id from the conversation.completed webhook to fetch it.
Limits and Errors#
Rate Limits#
{
"requests_per_minute": 60,
"daily_conversation_quota": 10000
}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 },
"retry_after_seconds": 60
}Error Envelope#
Documented error responses on /rest/* use the same envelope when applicable. Branch on a published 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" }
}Envelope fields:
code— stable, machine-readable identifier. Safe to branch on when present. We never rename or repurpose a published code. Generic infrastructure errors can omit this field.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).retry_after_seconds— deprecated top-level compatibility field on 429 responses. New clients should read details.retry_after_seconds and the Retry-After header.
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 - unexpected internal error; details are sanitized
- 502 - an upstream dependency returned a failing or invalid response
- 503 - a required service is temporarily unavailable
- 504 - an upstream or database request timed out
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 |
|---|---|---|
| 400 | invalid_meeting_url | Meeting URL is missing or is not a supported Zoom, Teams, or Google Meet URL |
| 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 |
| 403 | no_account_admin | No active account admin is available to own the notetaker request |
| 404 | account_not_found | The token's account no longer exists |
| 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) |
| 502 | notetaker_upstream_error | The notetaker provider returned a failing or invalid response |