Skip to main content
Skip to content
API reference

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.

Last reviewed · July 2026support@closeintel.ai

Reviewing us rather than building against us? Architecture, sub-processors, and compliance status live on the Security page.

Access

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.

01No scope required

Verify a key

GET/api/v1/me

Confirms 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.

Response 200
{
  "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.
02Requires ingest:write

Send a transcript

POST/api/v1/ingest

Creates 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.

Body
Body fields for POST /api/v1/ingest
FieldTypeDescription
emailstring, requiredThe rep's email. Must match a member this key may assign to.
titlestring, requiredUp to 200 characters. Shown to the rep as the call title.
transcriptstring, requiredAt 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_dateISO 8601 stringWhen the call happened. Must carry a UTC offset.
duration_secondsintegerUp to 28,800 (eight hours).
external_idstringYour own id for this call. Makes retries idempotent.
source_urlstringA link back to the call in your system.
transcript_segmentsarrayOptional speaker-labelled segments, each with speaker, text, start_time, end_time and char_offset.
Request
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"
  }'
Response 201
{ "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.
03Requires ingest:write

Send a recording

POST/api/v1/ingest/audio

Accepts 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.

Body
Body fields for POST /api/v1/ingest/audio
FieldTypeDescription
emailstring, requiredThe rep's email. Same resolution rules as a transcript.
titlestring, requiredUp to 200 characters.
audio_urlstring, requiredHTTPS, 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_dateISO 8601 stringWhen the call happened. Must carry a UTC offset.
duration_secondsintegerOptional. We use the measured duration when transcription reports one.
external_idstringYour own id. Omit it and we derive one from the submission.
source_urlstringA link back to the recording in your system.
Request
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"
  }'
Response 202
{
  "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.
04Requires ingest:write

Check a recording job

GET/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.

Response 200
{
  "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.
05Requires calls:read

Read analyzed calls

GET/api/v1/calls

Returns 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.

Query
Query fields for GET /api/v1/calls
FieldTypeDescription
sinceISO 8601 stringOnly calls analyzed after this moment. Must carry a UTC offset.
limitintegerBetween 1 and 100. Defaults to 25.
Request
curl "https://app.closeintel.ai/api/v1/calls?since=2026-07-26T09:00:00Z&limit=25" \
  -H "Authorization: Bearer ci_live_..."
Response 200
{
  "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.
Getting started

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

Already integrated?

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.

Error codes returned by the CloseIntel API
CodeStatusWhat it means
missing_credentials401No Authorization header, or it was not a bearer token.
invalid_api_key401The key does not match any active key.
revoked_api_key401The key existed and was revoked. Create a new one.
auth_rate_limited429Too many failed authentication attempts from this address.
rate_limit_per_key429This key passed 100 requests in a minute.
rate_limit_per_tenant429The workspace passed 100 requests in a minute across all keys.
insufficient_scope403The key lacks the capability this endpoint needs. The body names it.
validation_failed400A field was missing or malformed. The body lists each one with a reason.
invalid_json400The request body was not valid JSON.
payload_too_large413Over 5 MB for a transcript, or 100 KB of recording metadata.
audio_url_not_allowed400The recording URL is not HTTPS, is a share page rather than a file, or does not resolve to a public address.
email_not_found404No member matches that email. Set a default owner on the key to capture these instead.
member_scope_mismatch404The person exists but is outside the team this key is scoped to.
usage_limit_reached402The workspace has no analysis credits left this billing period.
job_not_found404No recording job with that id in this workspace.
partner_api_disabled503The 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.

Terminal error codes on a recording job
error_codeWhat happened
ERR_AUDIO_URLThe recording URL stopped being fetchable, or resolves somewhere we will not connect to.
ERR_TRANSCRIPTION_SUBMITWe could not hand the recording to our transcription provider. Retried automatically.
ERR_TRANSCRIPTIONThe provider reported a transcription failure.
ERR_TRANSCRIPTION_TIMEOUTTranscription did not finish inside its budget. Retried automatically.
ERR_NO_SPEECHThe audio contained no speech. Usually a voicemail tone or a silent file.
ERR_TRANSCRIPT_QUALITYIt transcribed, but is too short, too slow, or has only one speaker to analyze.
ERR_CALL_INSERTTranscription succeeded and the call could not be written. Retried automatically.
ERR_SUBMISSION_LOSTThe job was claimed but its provider reference was never recorded. Retried automatically.

Keep reading

Before you send us production data

Build 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.