Voicecape

API docs

The Voicecape REST API

Read summaries, transcripts, action items, and decisions, mark work done, and ask questions over your meetings — all from versioned, key-scoped endpoints under /v1.

API access is included on all plans (including Free) · OpenAPI spec: openapi.json · Plain-text index: llms.txt

Base URL and versioning

All v1 endpoints live under a fixed /v1 path. Additive changes (new fields, new endpoints) are non-breaking and will ship to v1; breaking changes go to a future /v2. Every response carries an x-request-id header — include it when contacting support.

https://api.recordport.app/v1

The API is designed for server-side consumption. Browsers from origins outside the Voicecape console allowlist do not receive CORS headers, so call it from your backend, not from front-end JavaScript.

Authentication

Authenticate with an API key sent as a Bearer token. Keys start with rp_ and are created in Console ▸ Integrations ▸ AI and API access. Treat keys like passwords and rotate any key you suspect has leaked.

Authorization: Bearer rp_your_key

A key's data scope depends on how it was issued:

  • ·Org keys read across the whole organization, excluding other members' private recordings.
  • ·Personal keys are bound to the member who created them and return only that member's own summaries — including their private ones. PATCH /v1/action-items/:id records that member as done_by when completing an item; other writes do not attribute an actor.
  • ·Department-scoped keys return only summaries assigned to that department.

Keys can also carry an expiry date; expired keys are rejected exactly like revoked ones. Call GET /v1/me to see what a key can do.

Scopes

Each key carries a set of scopes chosen at creation. Scopes are endpoint gates, not response-field redaction: a scope decides whether an endpoint answers at all, and never removes fields from a response you are allowed to read. Calling an endpoint without the required scope returns 403 insufficient_scope.

Scope Grants
summaries:readList and read summaries
transcripts:readRead full transcripts of summaries in scope
action_items:readList action items
action_items:writeMark action items done or not done
decisions:readList decisions
decisions:writeUpdate a decision's status
ask:runAsk questions over your meetings (POST /v1/ask)

The same scopes govern the MCP server tools — for example, complete_action_item requires action_items:write.

Rate limits

Limits are applied per API key over a sliding 60-second window. Exceeding a limit returns 429 rate_limited with a Retry-After: 30 header.

Category Limit Applies to
Read120 / minuteAll GET endpoints
Write60 / minutePATCH endpoints
Ask5 / minutePOST /v1/ask (plan quotas also apply)

Errors

Every error uses the same envelope. The request_id matches the x-request-id header.

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the required scope.",
    "request_id": "1f6c2a3e-9d4b-4c7e-8a21-0b5d9e6f3c11"
  }
}
Status Code Meaning
401unauthorizedMissing, invalid, revoked, or expired API key
403insufficient_scopeThe key does not carry the scope this endpoint requires
429rate_limitedRate limit exceeded — retry after the Retry-After header
404not_foundNo such resource, or the resource is outside this key's scope
400invalid_cursorThe cursor parameter is malformed — restart from the first page
400invalid_requestA parameter or body field failed validation

A resource that exists but is outside your key's scope returns the same 404 not_found as one that does not exist — the API does not reveal the existence of data you cannot read. Server-side failures return 500 query_failed or 503 server_not_configured in the same envelope.

Pagination

List endpoints return a data array plus a next_cursor. Treat cursors as opaque: pass the value back unchanged as the cursor parameter to fetch the next page. When next_cursor is null, you have reached the end. limit accepts 1–100 and defaults to 25.

# Call 1 — first page
curl -H "Authorization: Bearer rp_your_key" \
  "https://api.recordport.app/v1/summaries?limit=25"

# → { "data": [ …25 summaries… ],
#     "next_cursor": "MjAyNi0wOC0xMnw3ZDRlOWIyYS0xYzNmLTRlNWEtOWI2ZC0yZjhhMWMwZTdiM2Q" }

# Call 2 — next page, cursor passed back verbatim
curl -H "Authorization: Bearer rp_your_key" \
  "https://api.recordport.app/v1/summaries?limit=25&cursor=MjAyNi0wOC0xMnw3ZDRlOWIyYS0xYzNmLTRlNWEtOWI2ZC0yZjhhMWMwZTdiM2Q"

# → { "data": [ …older summaries… ], "next_cursor": null }

Endpoints

Endpoint Scope
GET /v1/meauthentication only
GET /v1/summariessummaries:read
GET /v1/summaries/:idsummaries:read
GET /v1/summaries/:id/transcripttranscripts:read
GET /v1/action-itemsaction_items:read
PATCH /v1/action-items/:idaction_items:write
GET /v1/decisionsdecisions:read
PATCH /v1/decisions/:iddecisions:write
POST /v1/askask:run

GET /v1/me

Describes the calling key: which organization it belongs to, its scopes, department binding, whether it is a personal key, and its expiry. No scope required beyond a valid key. scopes: null means a legacy key with full access.

curl -H "Authorization: Bearer rp_your_key" \
  https://api.recordport.app/v1/me
{
  "org": {
    "id": "0b6a4c2e-1d3f-4a5b-8c7d-9e0f1a2b3c4d",
    "name": "Acme Inc",
    "plan": "team"
  },
  "key": {
    "name": "Reporting bot",
    "scopes": ["summaries:read", "transcripts:read", "action_items:read", "decisions:read"],
    "dept_id": null,
    "user_scoped": false,
    "expires_at": "2027-08-16T00:00:00.000Z",
    "created_at": "2026-08-16T09:12:00.000Z"
  }
}

GET /v1/summaries

Lists summaries the key can read, newest first (by meeting date). Filters: from and to (YYYY-MM-DD, inclusive), dept_id (UUID), plus limit and cursor.

curl -H "Authorization: Bearer rp_your_key" \
  "https://api.recordport.app/v1/summaries?from=2026-08-01&to=2026-08-16&limit=25"
{
  "data": [
    {
      "id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
      "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
      "title": "Weekly sales sync",
      "occurred_on": "2026-08-12",
      "dept_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "visibility": "dept",
      "needs_review": false,
      "model": "gpt-5-mini",
      "summary_md": "## Weekly sales sync\n\nPipeline coverage recovered to 3.1x…",
      "key_points": ["Pipeline coverage recovered to 3.1x for Q3"],
      "decisions": ["Move the pricing update to the July release"],
      "action_items": ["Send the security overview to both trial accounts (Dana, 2026-08-20)"],
      "participants": ["Dana", "Minho"],
      "action_items_struct": [
        { "text": "Send the security overview to both trial accounts", "assignee": "Dana", "due": "2026-08-20" }
      ],
      "decisions_struct": [
        { "text": "Move the pricing update to the July release", "status": "confirmed", "decided_on": "2026-08-12" }
      ]
    }
  ],
  "next_cursor": "MjAyNi0wOC0xMnw3ZDRlOWIyYS0xYzNmLTRlNWEtOWI2ZC0yZjhhMWMwZTdiM2Q"
}

key_points, decisions, and action_items are flattened display strings; action_items_struct and decisions_struct carry the structured originals (assignee, due date, status) and are always present, as [] when empty.

GET /v1/summaries/:id

Returns one summary in the same shape as the list, plus chapters (omitted from lists to keep payloads small).

curl -H "Authorization: Bearer rp_your_key" \
  https://api.recordport.app/v1/summaries/7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d
{
  "data": {
    "id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
    "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
    "title": "Weekly sales sync",
    "occurred_on": "2026-08-12",
    "dept_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "visibility": "dept",
    "needs_review": false,
    "model": "gpt-5-mini",
    "summary_md": "## Weekly sales sync\n\nPipeline coverage recovered to 3.1x…",
    "key_points": ["Pipeline coverage recovered to 3.1x for Q3"],
    "decisions": ["Move the pricing update to the July release"],
    "action_items": ["Send the security overview to both trial accounts (Dana, 2026-08-20)"],
    "participants": ["Dana", "Minho"],
    "action_items_struct": [
      { "text": "Send the security overview to both trial accounts", "assignee": "Dana", "due": "2026-08-20" }
    ],
    "decisions_struct": [
      { "text": "Move the pricing update to the July release", "status": "confirmed", "decided_on": "2026-08-12" }
    ],
    "chapters": [
      { "title": "Pipeline review", "start": 0 },
      { "title": "Enterprise trials", "start": 812 }
    ]
  }
}

GET /v1/summaries/:id/transcript

Returns the full transcript for the recording behind a summary, with speaker names applied where the team has labeled them. If no transcript exists — or the summary is outside your key's scope — the response is the standard 404 not_found.

curl -H "Authorization: Bearer rp_your_key" \
  https://api.recordport.app/v1/summaries/7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d/transcript
{
  "data": {
    "summary_id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
    "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
    "text": "Dana: Pipeline coverage is back to 3.1x for Q3.\nMinho: Both enterprise trials asked for a security review…"
  }
}

GET /v1/action-items

Lists action items across summaries the key can read, newest first. Filters: done (true|false), summary_id (UUID), due_before and due_after (YYYY-MM-DD), plus limit and cursor.

curl -H "Authorization: Bearer rp_your_key" \
  "https://api.recordport.app/v1/action-items?done=false&due_before=2026-08-31"
{
  "data": [
    {
      "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
      "summary_id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
      "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
      "text": "Send the security overview to both trial accounts",
      "done": false,
      "done_at": null,
      "due_date": "2026-08-20",
      "assignee_user_id": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f",
      "occurred_on": "2026-08-12",
      "created_at": "2026-08-12T10:02:33.512041+00:00"
    }
  ],
  "next_cursor": null
}

PATCH /v1/action-items/:id

Marks an action item done or not done. The body must be {"done": true} or {"done": false}. When completed via a personal key, that member is recorded as the completer; org keys complete without attributing an actor.

curl -X PATCH \
  -H "Authorization: Bearer rp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"done": true}' \
  https://api.recordport.app/v1/action-items/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9
{
  "data": { "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "done": true }
}

GET /v1/decisions

Lists decisions across summaries the key can read, newest first. Filter by status (confirmed, revisit, or hold), plus limit and cursor.

curl -H "Authorization: Bearer rp_your_key" \
  "https://api.recordport.app/v1/decisions?status=confirmed"
{
  "data": [
    {
      "id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
      "summary_id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
      "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
      "text": "Move the pricing update to the July release",
      "status": "confirmed",
      "decided_on": "2026-08-12",
      "dept_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "occurred_on": "2026-08-12",
      "created_at": "2026-08-12T10:02:33.512041+00:00"
    }
  ],
  "next_cursor": null
}

PATCH /v1/decisions/:id

Updates a decision's status. The body must set status to confirmed, revisit, or hold — any other value returns 400 invalid_request.

curl -X PATCH \
  -H "Authorization: Bearer rp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "revisit"}' \
  https://api.recordport.app/v1/decisions/2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d
{
  "data": { "id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d", "status": "revisit" }
}

POST /v1/ask

Asks a natural-language question over the meetings your key can read and returns a cited answer. This endpoint shares its engine and plan quota with Ask in the console, and is limited to 5 requests per minute per key.

curl -X POST \
  -H "Authorization: Bearer rp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"question": "What did we decide about pricing this month?"}' \
  https://api.recordport.app/v1/ask
{
  "data": {
    "answer": "The pricing update was moved to the July release, confirmed in the Weekly sales sync on 2026-08-12…"
  }
}

If the ask engine declines (for example, the plan's monthly quota is exhausted), the response is a 400 with code ask_failed and a human-readable message.

Legacy export endpoint

GET /export/summaries.md is deprecated. It keeps working with the same keys (it now requires the summaries:read scope and shares the read rate limit), but new integrations should use GET /v1/summaries, which adds structured fields, cursor pagination, and the standard error envelope. Each summary's summary_md field carries the same Markdown the export produced.

Webhooks

Configure a webhook in Console ▸ Integrations and Voicecape will POST a JSON payload to your HTTPS endpoint whenever a summary is created or its content changes. Deliveries follow the Standard Webhooks specification, so off-the-shelf verification libraries work as-is.

Envelope

{
  "id": "msg_c3a9e1f0-7b2d-4c8e-9f1a-6d5b4c3a2e1f",
  "type": "summary.created",
  "timestamp": "2026-08-16T09:30:12.000Z",
  "data": {
    "org_id": "0b6a4c2e-1d3f-4a5b-8c7d-9e0f1a2b3c4d",
    "summary_id": "7d4e9b2a-1c3f-4e5a-9b6d-2f8a1c0e7b3d",
    "recording_id": "3c2b1a0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
    "title": "Weekly sales sync",
    "occurred_on": "2026-08-12",
    "dept_id": null,
    "visibility": "org",
    "needs_review": false,
    "model": "gpt-5-mini",
    "summary_md": "## Weekly sales sync\n…",
    "key_points": ["…"],
    "decisions": ["…"],
    "action_items": ["…"],
    "participants": ["…"],
    "action_items_struct": [ { "text": "…", "assignee": null, "due": null } ],
    "decisions_struct": [ { "text": "…", "status": null, "decided_on": null } ],
    "markdown": "## 2026-08-12 · Weekly sales sync\n…",
    "console_url": "https://app.voicecape.com/summaries?q=Weekly%20sales%20sync"
  }
}
Event When
summary.createdFirst successful delivery of a summary
summary.updatedThe summary's content changed after a re-run
summary.deletedReserved — not sent yet

Failed deliveries are retried with backoff. Retries reuse the same webhook-id, so deduplicate on it if your handler is not idempotent.

Signature headers

Every delivery carries three headers. The signature is an HMAC-SHA256 over the string {id}.{timestamp}.{body}, base64-encoded and prefixed with the version:

webhook-id: msg_c3a9e1f0-7b2d-4c8e-9f1a-6d5b4c3a2e1f
webhook-timestamp: 1786786212
webhook-signature: v1,K5oZfzN95Z9UVu1EsPQqUJoyv2HPeY9PYYQQgqqp8Eo=

Your signing secret is shown in the console when you connect the webhook, in the form whsec_<standard base64>. The HMAC key is the raw byte value: strip the whsec_ prefix and base64-decode the remainder — do not sign with the secret string itself.

Verifying deliveries

  1. 1Reject deliveries whose webhook-timestamp is more than 5 minutes from your clock, in either direction — this bounds replay attacks.
  2. 2Treat webhook-signature as a space-separated list and check each entry; if any one matches, the delivery is valid. During secret rotation Voicecape signs with both the old and new secret, so a single-entry check would break mid-rotation.
  3. 3Compare signatures with a constant-time comparison, never ===.
  4. 4Never return a 2xx status for a delivery that failed verification — a 2xx tells Voicecape the delivery succeeded and it will not be retried.
// Node — verify a Voicecape webhook (Standard Webhooks)
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.RP_WEBHOOK_SECRET; // "whsec_…"

function verify(headers, rawBody) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const sigHeader = headers["webhook-signature"] || "";
  if (!id || !ts || !sigHeader) return false;

  // 1. Reject timestamps outside ±5 minutes
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  // HMAC key = base64-decoded bytes after stripping the whsec_ prefix
  const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest(); // raw bytes

  // 2. Space-separated list — any match is valid (secret-rotation window)
  for (const part of sigHeader.split(" ")) {
    const [version, sig] = part.split(",");
    if (version !== "v1" || !sig) continue;
    const got = Buffer.from(sig, "base64");
    // 3. Constant-time comparison
    if (got.length === expected.length && timingSafeEqual(got, expected)) return true;
  }
  return false;
}

// 4. In your handler: if !verify(...), respond 401 — never 2xx.

MCP server

The same keys and scopes also power the Voicecape MCP server, which lets Claude, Cursor, and other MCP clients search and read your summaries directly, without writing any API code.

Create a key and make your first call

API keys live in Console ▸ Integrations ▸ AI and API access, ready in under a minute.

Start free

Already have an account? Sign in