HomeDocumentationAPI Reference
    Documentation/API Reference

    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#

    text
    REST API base URL: http://127.0.0.1:37837/api/v1/rest
    text
    OpenAPI spec: http://127.0.0.1:37837/api/v1/rest/openapi.json
    Note: Use the OpenAPI spec above as the canonical endpoint and schema reference.

    Authentication#

    Bearer token request
    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.

    Warning: External partner integrations should use /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.

    MethodPathScopeDescription
    GET/rest/accountnoneGet account and branding context
    GET/rest/usersusersList active users in account
    GET/rest/teamsteamsList teams with members
    GET/rest/departmentsdepartmentsList enabled departments
    GET/rest/conversationsconversationsList conversations (30-day window, cursor pagination)
    GET/rest/conversations/:idconversationsGet conversation detail (optional transcript/insights includes)
    GET/rest/conversations/:id/transcripttranscriptsGet transcript payload for one conversation
    GET/rest/conversations/:id/insightsinsightsGet insights/topics for one conversation
    GET/rest/conversations/:id/recording-urlrecordings + include_recordings=trueGet recording URL if available
    GET/rest/conversations/:id/summaryconversationsGet stored summary text for one conversation
    POST/rest/notetaker/requestnotetakerRequest notetaker join for supported meeting URL
    GET/rest/webhooks/deliveriesnoneList 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 recordings scope on the token and
    • the include_recordings = true setting 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#

    Request
    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"
    Response
    {
      "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 shape follows stored 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#

    Headers on every delivery attempt
    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#

    1. Read the raw request body before any JSON parsing.
    2. 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).
    3. Compare expected against the hex value in X-Webhook-Signature using a constant-time comparison (for example hmac.compare_digest in Python or crypto.timingSafeEqual in Node.js).
    4. 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.
    5. Treat repeated X-Webhook-Delivery-Id values as the same delivery and skip re-processing.
    Node.js verification snippet
    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#

    Example 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
        }
      }
    }
    Scope-conditional fields: Some fields are only included when the subscriptions has the matching scope:
    • transcript requires the transcripts scope.
    • insights requires the insights scope.
    • recording_url requires the recordings scope. and include_recordings = true on 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 the X-Webhook-Delivery-Id header. 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_prefix keep working. Set to the prefix of a same-account API token that has the same webhook URL when one exists, otherwise null. Use subscription_id instead. 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.

    Example payload
    {
      "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-000000000000 and never resolves to a real conversation.
    • Always check event / X-Webhook-Event before 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#

    json
    {
      "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.

    429 response (truncated)
    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.

    Example error payload
    {
      "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 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 example required_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.

    StatuscodeWhen
    400invalid_meeting_urlMeeting URL is missing or is not a supported Zoom, Teams, or Google Meet URL
    401missing_api_tokenNo Authorization header on the request
    401invalid_api_tokenToken does not match any record
    401token_inactiveToken exists but is disabled
    401token_revokedToken has been revoked
    401token_expiredToken has passed its expires_at
    403missing_scopeToken is missing the scope required by this endpoint
    403recordings_not_allowedToken has 'recordings' scope but include_recordings=false
    403quota_exceededDaily unique-conversation quota reached for this token
    403no_account_adminNo active account admin is available to own the notetaker request
    404account_not_foundThe token's account no longer exists
    404conversation_not_foundConversation does not exist or is not visible to this token
    429rate_limit_exceededPer-minute request rate limit hit (honor Retry-After)
    502notetaker_upstream_errorThe notetaker provider returned a failing or invalid response

    Support#

    Note: Questions about integration behavior or contract details? Email [email protected].