Send a call in. Read the coaching back out.
A small REST API for pushing transcripts and recordings into CloseIntel, and for reading analyzed calls once scoring finishes. Two write endpoints, one poll endpoint, one bearer key.
Reviewing us rather than building against us? Architecture, sub-processors, and compliance status live on the Security page.
Available to any workspace on request
API keys are created by a workspace admin under Configuration, then Integrations. The versioned endpoints below are enabled per environment while the partner program is in early access, so contact us before you build against them and we will turn the surface on for your workspace.
The reference
Five endpoints, and what each one is for
Jump to the one you need. Every response is JSON, every error carries a stable machine-readable code, and every write is safe to retry.
Authentication
Send your key as a bearer token: Authorization: Bearer ci_live_… Keys are workspace-scoped, and a key created for one team can only act on that team. We store a hash, never the key itself, so a lost key is replaced rather than recovered.
Scopes
Every key carries explicit capabilities. ingest:write permits the two write endpoints; calls:read permits reading analyzed calls back. A key without the scope it needs gets 403 and an insufficient_scope code naming what was missing.
Rate limits
One hundred requests per minute per key, and one hundred per minute per workspace. Both apply. A 429 carries a Retry-After header and a retry_after value in seconds. Failed authentication is counted separately and much more tightly, so a wrong key backs off fast without touching your real quota.
Idempotency
Send external_id and we will never create the same call twice. Omit it and we derive one by hashing the submission, which makes an ordinary network retry safe by default. A repeat submission answers 200 with the original record rather than an error.
Recordings are fetched, not uploaded
The recording endpoint takes a URL, not bytes. It must be HTTPS, publicly reachable, and a direct link to the media rather than a share page. Files up to 100 MB are accepted, which matches what Zapier will hand us.
Endpoints
The reference
Base URL is https://app.closeintel.ai. Paths are versioned, and the version only moves for a breaking change.
Verify a key
/api/v1/meConfirms a key is live and reports what it can do. Deliberately scope-free, because a connection test runs before anyone knows which scopes a key carries.
{
"tenant_id": "8f1c...",
"tenant_name": "Acme Corp",
"tenant_slug": "acme",
"key_label": "Zapier",
"source_name": "Zapier",
"scopes": ["ingest:write", "calls:read"],
"team": null,
"scope_type": "tenant",
"key_created_at": "2026-07-01T14:02:11.000Z",
"key_last_used_at": "2026-07-26T09:15:40.000Z"
}- Use this as your connection test. It never writes anything.
Send a transcript
/api/v1/ingestCreates a call from text you already have and queues it for analysis. The response returns as soon as the call is queued, well before scoring finishes.
| Field | Type | Description |
|---|---|---|
| string, required | The rep's email. Must match a member this key may assign to. | |
| title | string, required | Up to 200 characters. Shown to the rep as the call title. |
| transcript | string, required | At least 200 characters and 50 words, up to 2,000,000 characters. Shorter text cannot be analyzed and is rejected here rather than silently failing later. |
| call_date | ISO 8601 string | When the call happened. Must carry a UTC offset. |
| duration_seconds | integer | Up to 28,800 (eight hours). |
| external_id | string | Your own id for this call. Makes retries idempotent. |
| source_url | string | A link back to the call in your system. |
| transcript_segments | array | Optional speaker-labelled segments, each with speaker, text, start_time, end_time and char_offset. |
curl -X POST https://app.closeintel.ai/api/v1/ingest \
-H "Authorization: Bearer ci_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "rep@yourcompany.com",
"title": "Discovery — Acme Corp",
"transcript": "Rep: Thanks for making time today...",
"duration_seconds": 1800,
"external_id": "dialer-call-12345"
}'{ "call_id": "3b90...", "status": "queued" }- A repeat of the same external_id answers 200 with { "call_id": "...", "status": "duplicate" } and creates nothing.
- Bodies over 5 MB are rejected with payload_too_large.
- Sensitive data is removed from the transcript according to your workspace settings before it is stored. Payment card numbers are always removed.
Send a recording
/api/v1/ingest/audioAccepts a URL to an audio file, answers immediately with a job id, and transcribes in the background. Nothing is downloaded during your request, so the call returns in well under a second no matter how large the file is.
| Field | Type | Description |
|---|---|---|
| string, required | The rep's email. Same resolution rules as a transcript. | |
| title | string, required | Up to 200 characters. |
| audio_url | string, required | HTTPS, publicly reachable, and a direct link to the media. Share pages from Drive, Dropbox, OneDrive and Slack are rejected because they serve HTML rather than audio. |
| call_date | ISO 8601 string | When the call happened. Must carry a UTC offset. |
| duration_seconds | integer | Optional. We use the measured duration when transcription reports one. |
| external_id | string | Your own id. Omit it and we derive one from the submission. |
| source_url | string | A link back to the recording in your system. |
curl -X POST https://app.closeintel.ai/api/v1/ingest/audio \
-H "Authorization: Bearer ci_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "rep@yourcompany.com",
"title": "Discovery — Acme Corp",
"audio_url": "https://recordings.example.com/abc123.mp3"
}'{
"job_id": "b41d...",
"status": "pending",
"status_url": "/api/v1/jobs/b41d...",
"resolution_method": "direct_api_ingest"
}- A 202 means accepted, not finished. Poll status_url to find out whether it became a call.
- Files up to 100 MB are accepted. Larger recordings fail the job rather than the request.
- resolution_method reads owner_fallback when the email did not match and the key’s default owner absorbed the call.
Check a recording job
/api/v1/jobs/{id}Reports where a submitted recording stands, and why it stopped if it did. Status moves pending, then transcribing, then either completed with a call_id or failed with an error_code.
{
"job_id": "b41d...",
"status": "completed",
"external_id": "auto_9f2c...",
"title": "Discovery — Acme Corp",
"requested_email": "rep@yourcompany.com",
"resolution_method": "direct_api_ingest",
"attempts": 0,
"error_code": null,
"error_message": null,
"call_id": "3b90...",
"call_url": "https://app.closeintel.ai/dashboard/calls/3b90...",
"created_at": "2026-07-26T09:14:02.000Z",
"updated_at": "2026-07-26T09:16:48.000Z",
"transcription_started_at": "2026-07-26T09:14:09.000Z"
}- A job id belonging to another workspace answers 404 rather than confirming it exists.
- Failures that can be retried are retried for you. attempts tells you how many have been spent.
- Finished jobs are kept for 90 days; after that the id answers 404. The call itself is unaffected.
Read analyzed calls
/api/v1/callsReturns calls that have finished analysis, newest first. Built for polling: pass the analyzed_at of the newest item you have already seen as since, and deduplicate on id.
| Field | Type | Description |
|---|---|---|
| since | ISO 8601 string | Only calls analyzed after this moment. Must carry a UTC offset. |
| limit | integer | Between 1 and 100. Defaults to 25. |
curl "https://app.closeintel.ai/api/v1/calls?since=2026-07-26T09:00:00Z&limit=25" \ -H "Authorization: Bearer ci_live_..."
{
"calls": [
{
"id": "3b90...",
"title": "Discovery — Acme Corp",
"call_date": "2026-07-26",
"duration_seconds": 1800,
"source_type": "webhook",
"source_name": "Zapier",
"rep_email": "rep@yourcompany.com",
"overall_score": 82,
"executive_summary": "Strong discovery, weak next step.",
"call_outcome": "follow_up",
"analyzed_at": "2026-07-26T09:31:12.000Z",
"url": "https://app.closeintel.ai/dashboard/calls/3b90..."
}
],
"has_more": false
}- has_more tells a full page apart from the last one, so you can keep walking rather than guessing.
- A team-scoped key sees only that team. A team with no members returns an empty list, never the whole workspace.
Create a key
A workspace admin creates keys under Configuration, then Integrations. Pick the capabilities the integration needs and, if you want unmatched emails captured rather than rejected, a fallback owner.
Choose the team the key may act on
Grant only the scopes you need
Copy the key once. It is never shown again
The original webhook endpoint still works
POST /api/webhooks/ingest predates the versioned API and accepts the same transcript payload with the same key. It is not going anywhere, and it is not affected by early-access gating. New integrations should prefer /api/v1/ingest.
Errors
Every code this API returns
Branch on code, not on the message. Messages are written for humans and may be reworded; codes are part of the contract.
| Code | Status | What it means |
|---|---|---|
| missing_credentials | 401 | No Authorization header, or it was not a bearer token. |
| invalid_api_key | 401 | The key does not match any active key. |
| revoked_api_key | 401 | The key existed and was revoked. Create a new one. |
| auth_rate_limited | 429 | Too many failed authentication attempts from this address. |
| rate_limit_per_key | 429 | This key passed 100 requests in a minute. |
| rate_limit_per_tenant | 429 | The workspace passed 100 requests in a minute across all keys. |
| insufficient_scope | 403 | The key lacks the capability this endpoint needs. The body names it. |
| validation_failed | 400 | A field was missing or malformed. The body lists each one with a reason. |
| invalid_json | 400 | The request body was not valid JSON. |
| payload_too_large | 413 | Over 5 MB for a transcript, or 100 KB of recording metadata. |
| audio_url_not_allowed | 400 | The recording URL is not HTTPS, is a share page rather than a file, or does not resolve to a public address. |
| email_not_found | 404 | No member matches that email. Set a default owner on the key to capture these instead. |
| member_scope_mismatch | 404 | The person exists but is outside the team this key is scoped to. |
| usage_limit_reached | 402 | The workspace has no analysis credits left this billing period. |
| job_not_found | 404 | No recording job with that id in this workspace. |
| partner_api_disabled | 503 | The versioned API is not switched on for this environment yet. |
Recording jobs
Why a recording never became a call
Transcription happens after we answer, so its failures surface on the job rather than in the original response. Poll the job status endpoint and read error_code.
| error_code | What happened |
|---|---|
| ERR_AUDIO_URL | The recording URL stopped being fetchable, or resolves somewhere we will not connect to. |
| ERR_TRANSCRIPTION_SUBMIT | We could not hand the recording to our transcription provider. Retried automatically. |
| ERR_TRANSCRIPTION | The provider reported a transcription failure. |
| ERR_TRANSCRIPTION_TIMEOUT | Transcription did not finish inside its budget. Retried automatically. |
| ERR_NO_SPEECH | The audio contained no speech. Usually a voicemail tone or a silent file. |
| ERR_TRANSCRIPT_QUALITY | It transcribed, but is too short, too slow, or has only one speaker to analyze. |
| ERR_CALL_INSERT | Transcription succeeded and the call could not be written. Retried automatically. |
| ERR_SUBMISSION_LOST | The job was claimed but its provider reference was never recorded. Retried automatically. |
Keep reading
Before you send us production data
How your call data is handled
Tenant isolation, sensitive-data removal, and what our AI providers may and may not do with your content.
Read moreCompliance status and sub-processors
Current certification posture, the vendors that touch your data, and how to request our security questionnaire response.
Read moreWhat an analyzed call costs
Every call you send consumes one analysis credit from the workspace that owns it, whichever endpoint delivered it.
Read moreBuild with us
Need an endpoint we do not have?
We would rather hear what you are trying to build than guess. Tell us the integration and we will tell you honestly whether the API can do it today.