Quick start
Three steps to your first API call:
- Subscribe to a PRO or BUSINESS plan from /billing.
- Generate an API key from /account/api-keys and copy it (shown once).
- Call the API with the key in the Authorization header.
curl https://ccr.confluent-digital.com/api/v1/projects \
-H "Authorization: Bearer ccr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Authentication
All endpoints require an API key passed in the Authorization header using the Bearer scheme. Keys are bound to one organization; revoking a key is immediate.
Authorization: Bearer ccr_live_<32 hex chars>Lost a key? Revoke it from /account/api-keys and create a new one. The plain token is only visible at creation time — the server stores a SHA-256 hash.
Resources
Six endpoints, all read-only. Cross-organization access is blocked at the database level (a key from organization A receives 404 on a resource owned by organization B).
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/projects | List projects (paginated). |
| GET | /api/v1/projects/{id} | Single project detail. |
| GET | /api/v1/audios | List audio files. Filter by projectId and/or status. |
| GET | /api/v1/audios/{id} | Single audio detail. Includes the linked analysisId if any. |
| GET | /api/v1/analyses | List analyses. Filter by projectId and a createdAt range (from / to). |
| GET | /api/v1/analyses/{id} | Single analysis with criteria, recommendations and transcript. Raw Edith payload is intentionally not exposed. |
Pagination
All list endpoints use the same page/limit envelope. limit max is 100, default 50.
GET /api/v1/audios?page=2&limit=50&projectId=<id>&status=DONE
200 OK
{
"data": [
{
"id": "ckxxx...",
"projectId": "ckyyy...",
"originalName": "call_001.mp3",
"size": 2483920,
"source": "UPLOAD",
"status": "DONE",
"errorMessage": null,
"createdAt": "2026-05-10T14:32:11.000Z",
"processedAt": "2026-05-10T14:33:02.000Z",
"analysisId": "ckzzz..."
}
],
"pagination": { "page": 2, "limit": 50, "total": 137 }
}Filters
projectId— scope to a single project (cuid).status— audio status: PENDING, PROCESSING, DONE or FAILED.from,to— inclusive bounds on createdAt, ISO-8601 strings.
Webhooks (BUSINESS plan)
Subscribe an HTTPS endpoint to platform events. We POST signed JSON to your URL whenever a subscribed event fires — no polling required. Manage endpoints at /account/webhooks (owner only, BUSINESS plan).
Events
| Method | Path | Description |
|---|---|---|
| analysis.completed | POST | Fires after an audio finished analysis successfully. |
| analysis.failed | POST | Fires when an audio analysis terminates with a failure. |
| alert.created | POST | Fires when one of your alert rules matches a new analysis. |
| webhook.ping | POST | Synthetic test event triggered by the test button on /account/webhooks. |
Payload format
All deliveries share the same envelope: an envelope with id, event, created_at and an event-specific data block. The HTTP body is the JSON below; headers carry the event name, the delivery id and the HMAC signature.
POST https://yourapp.example.com/webhooks/ccr
Content-Type: application/json
User-Agent: CallCenterRate-Webhook/1.0
X-CCR-Event: analysis.completed
X-CCR-Delivery-Id: ckdelivery123...
X-CCR-Signature: t=1715423112,v1=8f3a...c9d2
{
"id": "ckdelivery123...",
"event": "analysis.completed",
"created_at": "2026-05-11T12:25:12.000Z",
"data": {
"analysis_id": "ckanalysis456...",
"audio_file_id": "ckaudio789...",
"project_id": "ckproject000...",
"overall_score": 78.5
}
}Signature verification
We sign every body with HMAC-SHA256 using your signing secret. The X-CCR-Signature header has the form t=[unix-timestamp],v1=[hex]. Compute HMAC(secret, timestamp + "." + rawBody) and constant-time compare. Reject requests where the timestamp drifts more than 300 seconds — that prevents replay attacks.
// Node.js — vérifier la signature reçue
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const [tPart, v1Part] = header.split(",");
const timestamp = tPart.split("=")[1];
const signature = v1Part.split("=")[1];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
}Retry & delivery guarantees
Deliveries are queued and retried with exponential backoff on transport errors and on any HTTP response that is not 2xx. We retry up to 6 times across about 24 hours.
- Backoff: 0s, 1min, 5min, 30min, 2h, 6h, 24h.
- We follow no redirects and reject private IPs (SSRF guard) and any non-HTTPS URL.
- Total delivery timeout per attempt is 10 seconds. Your endpoint should respond fast and process asynchronously.
Errors
All errors return a JSON body with a stable error code. The HTTP status reflects the error class.
{
"error": {
"code": "plan_required",
"message": "API access requires the PRO or BUSINESS plan."
}
}| HTTP | code | Description |
|---|---|---|
| 401 | unauthorized | API key is missing, malformed, unknown or revoked. |
| 403 | plan_required | The organization is not on a plan that grants API access. Upgrade to PRO or BUSINESS. |
| 429 | rate_limit_exceeded | Monthly API call quota exceeded. Wait for the period reset (header `Retry-After` in seconds) or upgrade the plan. |
| 404 | not_found | The resource does not exist or belongs to another organization. |
| 400 | validation_error | Query parameter or body validation failed. The message details which field. |
Quota & rate limiting
Each plan has a monthly API call quota (PRO: 10,000 / BUSINESS: 100,000). The counter resets on the same cycle as the analyses quota. On overuse, the API returns 429 with a `Retry-After` header (seconds until reset). Per-minute burst limits will be added in a future revision.
HTTP/1.1 429 Too Many Requests
Retry-After: 1209600
Content-Type: application/json
{
"error": {
"code": "rate_limit_exceeded",
"message": "Monthly API call quota exceeded (10000/10000). Resets on 2026-06-01T00:00:00.000Z."
}
}