Open Caven

Reference

On-prem connector endpoints

Media and connector endpoints used by the Caven on-prem connector

8 min read

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:

ScopeAllows
media:listSeeing which media files a recording has
media:downloadDownloading those files, still encrypted
media:keyRetrieving the key to decrypt a file, wrapped so only your connector can use it
connector:registerRegistering 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: 8c9d4aaabb273d033b27833ecdf6751c464f240f7e9c0421b7d43d46c625154b

Every 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=true

Returns 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/media

Scope: 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
      }
    ]
  }
}
FieldDescription
partID to use in the download and key URLs: primary, audio, thumbnail or hls:<file>
encryptedtrue if the stored bytes are encrypted. Thumbnails and HLS playlists are not encrypted.
encryptedSizeSize of the stored file in bytes
plaintextSizeSize after decryption: encryptedSize minus 28
etagUse it to check that a download is complete and unchanged
optionaltrue for parts a complete copy can do without

Download a media part

GET /recordings/:id/media/:part

Scope: 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-1023 or bytes=1024-) returns 206 Partial Content with a Content-Range header.
  • A range with several parts (bytes=0-9,20-29) or past the end of the file returns 416.
  • Add ?download=true to get a Content-Disposition: attachment header.

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/key

Scope: 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 get 409 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/organization

Scope: 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/register

Scope: connector:register

FieldRequiredDescription
publicKeyPemYesRSA public key, at least 2048 bits, in PEM format. At most 8192 bytes.
fingerprintNoIf you send it, Caven checks it against the key
connectorVersionNoShown on the On-prem page
storageModeNoShown on the On-prem page
notifyUrlNoAn 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
notifySecretWith notifyUrl32 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 notifyUrl turns 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 notifySecret is never returned.
  • See Notifications for what Caven sends to notifyUrl.
  • notifyUrl must 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 than http or https. Private ranges such as 10.0.0.0/8 and 192.168.0.0/16 are 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/source

Scope: 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/encryption

Scope: 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/backfill

Scope: 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:

HeaderValue
content-typeapplication/json
x-caven-eventrecording.ready
x-caven-signaturesha256= 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:

MethodPathReturns
GET/onprem-installer.shThe on-prem installer script
GET/onprem-installer.sh.sha256The 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.sh

Error codes

On top of the general errors:

StatuscodeWhen
400MISSING_PUBLIC_KEY, PUBLIC_KEY_TOO_LARGE, INVALID_PUBLIC_KEYpublicKeyPem is missing, too large or unreadable
400PRIVATE_KEY_SUBMITTEDA private key was sent. Only send the public half.
400UNSUPPORTED_KEY_TYPE, KEY_TOO_SMALLNot an RSA key, or smaller than 2048 bits
400FINGERPRINT_MISMATCHThe fingerprint sent doesn't match the key
400MISSING_NOTIFY_SECRET, NOTIFY_SECRET_TOO_LONGnotifySecret missing, under 32 or over 256 characters
400NOTIFY_URL_INVALID, NOTIFY_URL_SCHEME, NOTIFY_URL_CREDENTIALS, NOTIFY_URL_UNRESOLVED, NOTIFY_URL_BLOCKED, NOTIFY_URL_TOO_LONGnotifyUrl is rejected
400MISSING_FINGERPRINTHeartbeat without the X-Caven-Connector-Fingerprint header
400INVALID_BODY, INVALID_STATUSThe request body has the wrong shape
400NO_SOURCE_CONNECTORconnector/source: no registered connector to make the live source
403ONPREM_NOT_DOCKERconnector/source: the organization doesn't use Docker on-prem
403ONPREM_NOT_ENABLED, ONPREM_METHOD_NOT_DOCKEROn-prem isn't on for this organization, or doesn't use Docker
404UNKNOWN_MEDIA_PART, NO_MEDIAThe part doesn't exist, or the recording has no stored media
404MEDIA_PART_GONEThe part is listed, but no longer exists in storage
404KEY_NOT_FOUNDNo data key exists for this recording; it may not be stored encrypted
409NOT_MARKED_FOR_ONPREMThe recording isn't marked for on-prem delivery
409UPLOAD_INCOMPLETEThe recording hasn't finished uploading
409CONNECTOR_NOT_REGISTEREDNo active connector with this fingerprint. Register first.
409PART_NOT_ENCRYPTEDYou asked for the key of a part that isn't encrypted
409KEYS_HELD_BY_CAVENEncryption at rest is on, so keys aren't issued to the connector
409NOT_REGISTEREDconnector/source: no active connector registered this notifyUrl
409NO_ORGANIZATIONThe key isn't linked to an organization
409OWNER_UNRESOLVEDThe recording's owner has no email address, so Caven can't tell where its content is stored
416RANGE_NOT_SATISFIABLEThe Range header asks for more than one range, or for bytes outside the file
503SOURCE_UNREACHABLEconnector/source: the connector couldn't be reached over HTTPS
503KEY_SERVICE_UNAVAILABLEThe key service is temporarily unavailable. Try again.
503KEY_MALFORMEDThe 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.

© 2026 Caven BV. All rights reserved. Built in Belgium.EU · GDPR · AI Act · Privacy-first · ISO 27001

On this page