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/:idrecords that member asdone_bywhen 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:read | List and read summaries |
| transcripts:read | Read full transcripts of summaries in scope |
| action_items:read | List action items |
| action_items:write | Mark action items done or not done |
| decisions:read | List decisions |
| decisions:write | Update a decision's status |
| ask:run | Ask 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 |
|---|---|---|
| Read | 120 / minute | All GET endpoints |
| Write | 60 / minute | PATCH endpoints |
| Ask | 5 / minute | POST /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 |
|---|---|---|
| 401 | unauthorized | Missing, invalid, revoked, or expired API key |
| 403 | insufficient_scope | The key does not carry the scope this endpoint requires |
| 429 | rate_limited | Rate limit exceeded — retry after the Retry-After header |
| 404 | not_found | No such resource, or the resource is outside this key's scope |
| 400 | invalid_cursor | The cursor parameter is malformed — restart from the first page |
| 400 | invalid_request | A 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/me | authentication only |
| GET /v1/summaries | summaries:read |
| GET /v1/summaries/:id | summaries:read |
| GET /v1/summaries/:id/transcript | transcripts:read |
| GET /v1/action-items | action_items:read |
| PATCH /v1/action-items/:id | action_items:write |
| GET /v1/decisions | decisions:read |
| PATCH /v1/decisions/:id | decisions:write |
| POST /v1/ask | ask: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.created | First successful delivery of a summary |
| summary.updated | The summary's content changed after a re-run |
| summary.deleted | Reserved — 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
- 1Reject deliveries whose
webhook-timestampis more than 5 minutes from your clock, in either direction — this bounds replay attacks. - 2Treat
webhook-signatureas 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. - 3Compare signatures with a constant-time comparison, never
===. - 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 freeAlready have an account? Sign in