Reference
On-prem connector endpoints
Media and connector endpoints used by the Caven on-prem connector
These endpoints exist for the Caven on-prem connector, a container that runs on your own server and keeps a copy of your organization's recordings there. Normally the connector calls them, not you. This page documents them completely, so your IT team knows exactly what the connector key can do, and so you can build your own integration if you need to.
These endpoints only work for organizations with on-prem delivery turned on, using the Docker method. Otherwise they return 403 ONPREM_NOT_ENABLED, or 403 ONPREM_METHOD_NOT_DOCKER when the organization uses the SharePoint method.
The connector key
Create it on the organization's On-prem page with Create connector key. That button creates an organization key with exactly the scopes the connector needs, including the four that are never granted by default:
| Scope | Allows |
|---|---|
media:list | Seeing which media files a recording has |
media:download | Downloading those files, still encrypted |
media:key | Retrieving the key to decrypt a file, wrapped so only your connector can use it |
connector:register | Registering the connector and using the other /connector endpoints |
These scopes must be on the key explicitly. A key with an empty scope list doesn't get them. POST /recordings/:id/delivered also needs recordings:get explicitly.
Identifying the connector
The connector has an RSA key pair. Its fingerprint is the SHA-256 hash of the public key in DER SubjectPublicKeyInfo form, written as 64 hexadecimal characters.
The connector sends that fingerprint on every request:
X-Caven-Connector-Fingerprint: 8c9d4aaabb273d033b27833ecdf6751c464f240f7e9c0421b7d43d46c625154bEvery request with this header counts as a sign of life, and updates when the connector was last seen on the organization's On-prem page. To spare the database, that time is updated at most once every 60 seconds per connector.
Finding recordings to deliver
GET /recordings?onPremDelivery=trueReturns only the recordings marked for delivery to the Docker connector. A recording is marked when it's made while on-prem delivery is on, so a new installation starts on the day it was installed. Recordings made before that are only included after a backfill, which Caven runs for you.
With onPremDelivery=true, the list always uses cursor pagination, even on the first page. onPremDelivery=false returns only the recordings that are not marked.
Media
List a recording's media
GET /recordings/:id/mediaScope: media:list
{
"data": {
"recordingId": "cmtzpj5h058l0i6eyohsfrxit",
"parts": [
{
"part": "primary",
"kind": "primary",
"filename": "recording.webm",
"mimeType": "video/webm",
"encrypted": true,
"encryptedSize": 168176723,
"plaintextSize": 168176695,
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
"keyRef": "...",
"derived": false,
"optional": false
}
]
}
}| Field | Description |
|---|---|
part | ID to use in the download and key URLs: primary, audio, thumbnail or hls:<file> |
encrypted | true if the stored bytes are encrypted. Thumbnails and HLS playlists are not encrypted. |
encryptedSize | Size of the stored file in bytes |
plaintextSize | Size after decryption: encryptedSize minus 28 |
etag | Use it to check that a download is complete and unchanged |
optional | true for parts a complete copy can do without |
Download a media part
GET /recordings/:id/media/:partScope: media:download
Returns the stored bytes exactly as Caven keeps them. Encrypted parts are served as application/octet-stream.
- Range requests are supported, so a large download can resume after a dropped connection. A single range (
bytes=0-1023orbytes=1024-) returns206 Partial Contentwith aContent-Rangeheader. - A range with several parts (
bytes=0-9,20-29) or past the end of the file returns416. - Add
?download=trueto get aContent-Disposition: attachmentheader.
Encrypted parts use AES-256-GCM in this layout:
[ IV, 12 bytes ][ GCM tag, 16 bytes ][ ciphertext ]Get a part's decryption key
GET /recordings/:id/media/:part/keyScope: media:key
{
"data": {
"recordingId": "cmtzpj5h058l0i6eyohsfrxit",
"part": "primary",
"keyRef": "...",
"contentAlgorithm": "aes-256-gcm",
"wrap": {
"algorithm": "RSA-OAEP-256",
"fingerprint": "8c9d4aaabb273d033b27833ecdf6751c464f240f7e9c0421b7d43d46c625154b",
"wrappedKey": "base64..."
}
}
}wrappedKey is the part's 32-byte data key, encrypted with the registered connector's public key using RSA-OAEP with SHA-256. Only the connector's private key can unwrap it. Caven never returns a key in plain text.
- Optionally pass
?fingerprint=to require a specific connector. If it isn't the active one, you get409 CONNECTOR_NOT_REGISTERED. - The response has
Cache-Control: no-store. - Every key that is handed out is recorded in an audit log.
Connector endpoints
These endpoints return a bare JSON object, without the data wrapper. The exceptions are register and heartbeat.
Organization
GET /connector/organizationScope: organization:read. Used by the installer to confirm which organization the key belongs to.
{
"organizationId": "cmtih9d2h1pmti69atwa0my21",
"name": "Acme Legal",
"storeEncrypted": false,
"sourceEnabled": false,
"deleteAfterSync": false
}Register
POST /connector/registerScope: connector:register
| Field | Required | Description |
|---|---|---|
publicKeyPem | Yes | RSA public key, at least 2048 bits, in PEM format. At most 8192 bytes. |
fingerprint | No | If you send it, Caven checks it against the key |
connectorVersion | No | Shown on the On-prem page |
storageMode | No | Shown on the On-prem page |
notifyUrl | No | An http(s) address on your network that Caven calls when a recording is ready, so the connector doesn't have to wait for its next poll |
notifySecret | With notifyUrl | 32 to 256 characters. Proves that a notification comes from Caven. |
{
"data": {
"fingerprint": "8c9d4aaabb273d033b27833ecdf6751c464f240f7e9c0421b7d43d46c625154b",
"organizationId": "cmtih9d2h1pmti69atwa0my21",
"registeredAt": "2026-10-06T10:04:20.338Z",
"connectorVersion": "1.0.0",
"storageMode": "local-key",
"replacedPrevious": false,
"notifyUrl": "https://connector.example.com/events",
"notifyEnabled": true
}
}- Registering the same key again updates the registration. Leaving out
notifyUrlturns notifications off. - Registering a different key is treated as a security event: the previous connector is deactivated (not deleted) and stops receiving keys, and the response says
"replacedPrevious": true. - The
notifySecretis never returned. - See Notifications for what Caven sends to
notifyUrl. notifyUrlmust resolve in DNS (or be an IP address). It is rejected if it resolves to a loopback, link-local (including the cloud metadata address), multicast or unspecified address, contains a username or password, or uses a scheme other thanhttporhttps. Private ranges such as10.0.0.0/8and192.168.0.0/16are allowed, because the connector usually sits on your own network.
Heartbeat
POST /connector/heartbeat
X-Caven-Connector-Fingerprint: <fingerprint>Scope: connector:register. Reports the connector's state to the organization's On-prem page. Every field is optional:
{
"lastPollOk": true,
"pendingCount": 3,
"completedCount": 120,
"failedCount": 0,
"lastError": null,
"lastSyncedRecordingId": "cmtzpj5h058l0i6eyohsfrxit",
"diskFreeBytes": 512000000000,
"diskTotalBytes": 1000000000000,
"uptimeSeconds": 86400,
"connectorVersion": "1.0.0",
"storageMode": "local-key"
}Response: { "data": { "ok": true, "fingerprint": "...", "recordedAt": "..." } }
Confirm a delivery
POST /recordings/:id/delivered
X-Caven-Connector-Fingerprint: <fingerprint>Scope: recordings:get, granted explicitly. Tells Caven a recording is safely stored on your server, or that it failed or was skipped.
{ "status": "completed" }status is completed, failed or skipped. With failed or skipped, you can add an error text of up to 2000 characters.
Response: { "data": { "recordingId": "...", "status": "completed", "recordedAt": "..." } }
Confirming a delivery doesn't change the recording's updatedAt, so it doesn't come back in the next sync.
Live source
POST /connector/sourceScope: connector:register. The installer's last step. It makes the connector registered with this notifyUrl the organization's live source.
{ "notifyUrl": "https://connector.example.com/events", "deleteAfterSync": false }notifyUrl must start with https:// here; anything else returns 400 INVALID_BODY. Caven first checks that the connector can be reached over HTTPS. If not, you get 503 SOURCE_UNREACHABLE and nothing changes.
Response: { "enabled": true, "deleteAfterSync": false, "fingerprint": "..." }
Encryption at rest
POST /connector/encryptionScope: connector:register
{ "enabled": true }When this is on, the files on your server are encrypted with an organization key that stays at Caven, and the key endpoint stops issuing data keys to the connector (409 KEYS_HELD_BY_CAVEN).
Response: { "enabled": true, "keyHeldByCaven": true }
Backfill
POST /connector/backfillScope: connector:register. Marks the organization's existing recordings for delivery, so the connector also copies the history. Phone calls and content hosted in SharePoint are left out. Safe to repeat: a second call marks nothing new.
Response: { "marked": 26 }
Copying the history can take hours or days, so the current installer no longer calls this endpoint. Caven runs the backfill for you after the connector is installed. The endpoint stays available if you build your own integration.
Notifications
When you register a notifyUrl, Caven tells the connector as soon as a recording is ready, instead of waiting for its next poll. Caven sends a POST to notifyUrl with this JSON body:
{ "event": "recording.ready", "recordingId": "cmtzpj5h058l0i6eyohsfrxit", "sentAt": "2026-10-06T10:04:20.338Z" }and these headers:
| Header | Value |
|---|---|
content-type | application/json |
x-caven-event | recording.ready |
x-caven-signature | sha256= followed by the HMAC-SHA256 of the exact request body, in hexadecimal, signed with your notifySecret |
Check the signature over the raw body before you trust the request, and use sentAt to reject old, replayed notifications. Caven does not follow redirects: a notifyUrl that answers with a redirect counts as a failed notification. A notification is only a hint: the connector still fetches the recording through the endpoints above.
Installer and health check
These need no API key and live at the root of developers.caven.io, not under /public/v1:
| Method | Path | Returns |
|---|---|---|
| GET | /onprem-installer.sh | The on-prem installer script |
| GET | /onprem-installer.sh.sha256 | The SHA-256 checksum of that script, to verify your download |
| GET | /health | { "status": "ok", "service": "caven-public-api" } when the API is up |
Download the installer and then run it, don't pipe it into a shell:
curl -fsSLO https://developers.caven.io/onprem-installer.sh
sudo bash onprem-installer.shError codes
On top of the general errors:
| Status | code | When |
|---|---|---|
| 400 | MISSING_PUBLIC_KEY, PUBLIC_KEY_TOO_LARGE, INVALID_PUBLIC_KEY | publicKeyPem is missing, too large or unreadable |
| 400 | PRIVATE_KEY_SUBMITTED | A private key was sent. Only send the public half. |
| 400 | UNSUPPORTED_KEY_TYPE, KEY_TOO_SMALL | Not an RSA key, or smaller than 2048 bits |
| 400 | FINGERPRINT_MISMATCH | The fingerprint sent doesn't match the key |
| 400 | MISSING_NOTIFY_SECRET, NOTIFY_SECRET_TOO_LONG | notifySecret missing, under 32 or over 256 characters |
| 400 | NOTIFY_URL_INVALID, NOTIFY_URL_SCHEME, NOTIFY_URL_CREDENTIALS, NOTIFY_URL_UNRESOLVED, NOTIFY_URL_BLOCKED, NOTIFY_URL_TOO_LONG | notifyUrl is rejected |
| 400 | MISSING_FINGERPRINT | Heartbeat without the X-Caven-Connector-Fingerprint header |
| 400 | INVALID_BODY, INVALID_STATUS | The request body has the wrong shape |
| 400 | NO_SOURCE_CONNECTOR | connector/source: no registered connector to make the live source |
| 403 | ONPREM_NOT_DOCKER | connector/source: the organization doesn't use Docker on-prem |
| 403 | ONPREM_NOT_ENABLED, ONPREM_METHOD_NOT_DOCKER | On-prem isn't on for this organization, or doesn't use Docker |
| 404 | UNKNOWN_MEDIA_PART, NO_MEDIA | The part doesn't exist, or the recording has no stored media |
| 404 | MEDIA_PART_GONE | The part is listed, but no longer exists in storage |
| 404 | KEY_NOT_FOUND | No data key exists for this recording; it may not be stored encrypted |
| 409 | NOT_MARKED_FOR_ONPREM | The recording isn't marked for on-prem delivery |
| 409 | UPLOAD_INCOMPLETE | The recording hasn't finished uploading |
| 409 | CONNECTOR_NOT_REGISTERED | No active connector with this fingerprint. Register first. |
| 409 | PART_NOT_ENCRYPTED | You asked for the key of a part that isn't encrypted |
| 409 | KEYS_HELD_BY_CAVEN | Encryption at rest is on, so keys aren't issued to the connector |
| 409 | NOT_REGISTERED | connector/source: no active connector registered this notifyUrl |
| 409 | NO_ORGANIZATION | The key isn't linked to an organization |
| 409 | OWNER_UNRESOLVED | The recording's owner has no email address, so Caven can't tell where its content is stored |
| 416 | RANGE_NOT_SATISFIABLE | The Range header asks for more than one range, or for bytes outside the file |
| 503 | SOURCE_UNREACHABLE | connector/source: the connector couldn't be reached over HTTPS |
| 503 | KEY_SERVICE_UNAVAILABLE | The key service is temporarily unavailable. Try again. |
| 503 | KEY_MALFORMED | The stored data key is not valid. Contact Caven. |
Rate limits
Media and keys have their own budget, separate from the metadata endpoints. See Rate limits.