Reference
Requests and responses
Response format, pagination, incremental sync, errors and rate limits
Response format
Every successful response is a JSON object with a data field. Lists also carry a meta object:
{
"data": [ ... ],
"meta": { "total": 410, "limit": 20, "offset": 0 }
}A single object comes back as { "data": { ... } }. The only exceptions are four connector endpoints, which return a bare object.
- Timestamps are ISO 8601 in UTC, for example
2026-10-05T15:24:30.542Z. - Fields without a value are
null. They are never left out. - IDs are opaque strings. Don't parse them.
Pagination
Offset pagination
All list endpoints accept:
| Parameter | Default | Notes |
|---|---|---|
limit | 20 | 1 to 100. Higher values are capped at 100; 0 or a non-number means 20. |
offset | 0 | Number of items to skip |
meta.total is the total number of matching items. Recordings are returned newest first.
curl "https://developers.caven.io/public/v1/recordings?limit=50&offset=50" \
-H "X-API-Key: sk_your_key_here"Cursor pagination and incremental sync
Offset pagination isn't reliable for syncing: each new recording shifts every page by one, so you may see some recordings twice and miss others. For syncing, GET /recordings also supports cursor pagination. It switches on as soon as you pass updatedSince or cursor.
| Parameter | Description |
|---|---|
updatedSince | ISO 8601 date. Only recordings changed at or after this moment. |
cursor | The meta.nextCursor from the previous page |
limit | 1 to 100, default 20 |
In cursor mode, results are ordered by last change (oldest first), and meta looks like this:
{ "meta": { "limit": 20, "hasMore": true, "nextCursor": "MjAyNi0wMS0yMVQxMzoxODoxMy4zOTJafGNta28x..." } }To sync:
- Request
/recordings?updatedSince=<last sync time>. - While
hasMoreistrue, request/recordings?cursor=<nextCursor>. - Save the time you started as the next
updatedSince.
A recording also counts as changed when its transcript or summary is finished, so a recording that is still being processed comes back once it's done.
The cursor is opaque. Pass it back exactly as you received it. A cursor that has been modified returns 400 INVALID_CURSOR.
Errors
Errors use standard HTTP status codes and a consistent body:
{
"message": "Invalid status: completed. Expected one of: RECORDED, PROCESSING, TRANSCRIBED, SUMMARIZED, FAILED",
"code": "INVALID_PARAM",
"details": { "field": "status", "allowed": ["RECORDED", "PROCESSING", "TRANSCRIBED", "SUMMARIZED", "FAILED"] }
}Branch on code, not on message. Messages can change, codes don't.
| Status | code | When |
|---|---|---|
| 400 | INVALID_PARAM | A query parameter has an invalid value, such as a bad date or an unknown status |
| 400 | INVALID_CURSOR | The cursor or updatedSince value can't be read |
| 401 | API_KEY_REQUIRED, API_KEY_INVALID, API_KEY_EXPIRED | See Authentication |
| 403 | INSUFFICIENT_SCOPE | The key lacks the scope for this endpoint |
| 403 | ONPREM_NOT_ENABLED, ONPREM_METHOD_NOT_DOCKER | Media and connector endpoints, when the organization doesn't use Docker on-prem delivery |
| 404 | NOT_FOUND | The object doesn't exist, or the key can't see it |
| 404 | TRANSCRIPT_NOT_AVAILABLE, SUMMARY_NOT_AVAILABLE, NOTES_NOT_AVAILABLE | The recording exists, but this content doesn't (yet) |
| 404 | NO_ORGANIZATION | An organization endpoint, called with a key that has no organization |
| 409 | HOSTED_SOURCE_UNAVAILABLE | The content is stored only on your organization's own server and isn't available through Caven |
| 429 | RATE_LIMITED | Too many requests. See below. |
| 500 | INTERNAL_ERROR | Something went wrong on our side |
| 503 | HOSTED_SOURCE_UNREACHABLE | The content is stored outside Caven (for example in SharePoint) and that source couldn't be reached. Try again later. |
A recording that belongs to someone outside the key's scope returns 404, not 403. The API doesn't reveal whether it exists.
A 403 INSUFFICIENT_SCOPE for one of the media or connector scopes, which must be granted explicitly, carries an extra detail field (singular) with a plain explanation:
{
"message": "Scope 'media:download' required",
"code": "INSUFFICIENT_SCOPE",
"detail": "This scope must be granted explicitly on the API key. Keys with no scopes listed do not receive it. ..."
}The media and connector endpoints have more codes of their own. See On-prem connector.
Rate limits
Limits are counted per API key, so other customers' traffic never affects yours.
| Endpoints | Limit |
|---|---|
| All metadata endpoints (recordings, transcripts, organization, and so on) | 300 requests per minute |
| Media inventory and downloads | 1,200 requests per minute |
| Media keys | 600 requests per minute |
Every response includes the standard rate-limit headers:
RateLimit-Policy: 300;w=60
RateLimit-Limit: 300
RateLimit-Remaining: 299
RateLimit-Reset: 60When you exceed a limit you get 429 with code: "RATE_LIMITED". Wait RateLimit-Reset seconds before trying again.
On developers.caven.io there is also a coarse limit of 600 requests per minute per IP address for unauthenticated traffic, so a flood of requests without a valid key is stopped early.
CORS
On developers.caven.io the API sends Access-Control-Allow-Origin: *, so browsers can reach it. api.caven.io does not: it only accepts cross-origin requests from Caven's own web apps, and doesn't allow the X-API-Key header from a browser. Even so, never put an API key in code that runs in a browser: anyone who opens the page can read it. Call the API from your own server instead.