[ Cora Intelligence ]

Cora API — v1

Agent-first REST API for Cora, an AI workforce for outbound sales. Manage leads and AI agents, trigger real phone calls and emails, and read conversation history and metrics. Every endpoint maps 1:1 to an MCP tool, so anything documented here is also callable by an AI agent through the Cora MCP server.

  • Base URL: https://YOUR-APP/api/v1 — replace with your deployment. Local dev: http://localhost:3000/api/v1.
  • Auth: Authorization: Bearer cora_live_...
  • Content-Type: application/json for request bodies.
  • This doc (markdown): GET /api/v1/docs · Machine-readable index: GET /api/v1

If you're an AI agent (Claude, Cursor, …), connect through the Cora MCP server, not the raw REST API. Add Cora as a custom connector and you sign in once with Cora — no API key to manage, and every endpoint below is exposed as an MCP tool you can call directly.

  • Connector URL: https://YOUR-APP/api/mcp
  • Claude Code: claude mcp add --transport http cora https://YOUR-APP/api/mcp
  • Claude Desktop / Cursor: add a custom connector with that URL.

On first use a browser opens to sign in with Cora and approve access; the tools then work across all your projects. The MCP tool names match the operations in this reference (e.g. list_leads, get_lead, create_lead, trigger_call, send_email, get_call_audio, get_stats). The REST API below is the alternative for scripts/backends that hold an API key.


For AI agents — read this first

Cora's job is outbound sales: it stores leads (the people you're selling to), runs agents (AI workers that call and email leads), and records every call and email.

Typical flows:

  • "What's happening?"GET /v1/stats for totals, GET /v1/activity for the recent event feed.
  • "Find a lead"GET /v1/leads?query=jane. Then GET /v1/leads/{id} for full detail incl. recent calls, emails, and activity.
  • "Add leads"POST /v1/leads (one) or POST /v1/leads/bulk (up to 500; duplicates by email are skipped).
  • "Reach out"POST /v1/outreach/call or POST /v1/outreach/email. These place real calls / send real emails. Always confirm intent with the human before calling these. A call needs the lead to have a phone number and the agent to have a phone number assigned.
  • "Set up an agent"GET /v1/agents, POST /v1/agents, PATCH /v1/agents/{id}, then POST /v1/agents/{id}/assign-leads.

Rules of thumb: read endpoints are safe and free to call; write endpoints change real data; outreach endpoints have real-world side effects. Errors are descriptive (see Error codes) — read error.message and adjust, don't blindly retry.


Authentication

Pass your API key as a Bearer token on every request:

curl -H "Authorization: Bearer cora_live_..." https://YOUR-APP/api/v1/leads

Keys have a scope: full (read + write) or read_only (GET only). A read_only key calling a write endpoint returns 403 read_only_key. Create and revoke keys in the Cora app (project dashboard → API tab); the raw key is shown once at creation.

FailureStatuscode
No Authorization header401missing_api_key
Unknown / malformed key401invalid_api_key
Revoked key401revoked_api_key
read_only key on a write403read_only_key

Conventions

Response envelope

Every successful response wraps its payload under a top-level data key — both single-object and list endpoints. The per-endpoint "Response" examples below show the inner shape (what's under data).

{ "data": { ... } }        // single object
{ "data": [ ... ], "has_more": false, "next_cursor": null }   // list

Pagination

List endpoints return their items under data plus paging fields:

{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "eyJ0cyI6..."
}

Pass limit (1–100, default 50) and cursor (the previous response's next_cursor). When has_more is false, next_cursor is null.

Errors (Stripe-style)

Every error has the same envelope:

{
  "error": {
    "type": "invalid_request_error",
    "code": "lead_no_phone",
    "message": "This lead has no phone number.",
    "param": "lead_id",
    "status": 422
  }
}

Idempotency

Send Idempotency-Key: <unique-id> on write requests. A repeated key within 24h returns the cached original response instead of acting twice. It is required on POST /v1/outreach/call (missing → 400 missing_idempotency_key) and optional (but recommended) on creates.

Rate limits

120 requests/minute per key. Every response includes:

  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset (unix seconds)

Over the limit → 429 rate_limit_exceeded with retry_after (seconds) in the body.

Timestamps, IDs, headers

  • Timestamps — ISO 8601 UTC strings (or null).
  • IDs — opaque strings.
  • Request ID — every response carries X-Request-Id (include it when reporting issues).
  • CORS — open (*), safe because auth is header-based, not cookie-based.

Endpoints

Leads

List or search leads.

Params

  • query — matches name/email/phone.
  • status — stage filter: new | in_progress | converted.
  • agent_id — filter by assigned agent.
  • has_meetingtrue/false.
  • limit — page size.
  • cursor — pagination cursor.
curl -H "Authorization: Bearer $KEY" "https://YOUR-APP/api/v1/leads?query=acme&has_meeting=true&limit=20"

Response — each lead:

{
  "id": "...",
  "name": "...",
  "first_name": "...",
  "last_name": "...",
  "company": "...",
  "email": "...",
  "phone": "...",
  "stage": "...",
  "meeting_booked": false,
  "converted": false,
  "outreach_disabled": false,
  "assigned_user_uid": "...",
  "created_at": "...",
  "total_calls": null,
  "total_emails": null,
  "last_activity_at": null
}

total_* are null in list view; populated on the single-lead GET.

GET/v1/leads/{lead_id}full detail

Returns the lead plus its recent calls, emails, and activity arrays and accurate total_calls / total_emails / last_activity_at.

POST/v1/leadscreate

Create a lead. Provide at least one of name, first_name, last_name, company.

Body

{
  "name": "Jane Doe",
  "email": "jane@acme.com",
  "phone": "+14155550123",
  "company": "Acme",
  "role": "VP Sales",
  "stage": "new",
  "agent_id": "optional-agent",
  "custom_fields": {}
}

phone must be E.164 (+ then digits). Returns 201 with the created lead. Honors Idempotency-Key.

POST/v1/leads/bulkbulk create (≤ 500)

Create up to 500 leads at once. Duplicates by email are skipped.

Body

{
  "leads": [
    { "name": "...", "email": "..." }
  ],
  "agent_id": "optional"
}

Response

{
  "data": {
    "created": [ { "id": "...", "name": "...", "email": "..." } ],
    "count": 0,
    "duplicates": 0,
    "errors": 0
  }
}

PATCH/v1/leads/{lead_id}update

Send only the fields to change.

Body

  • name — lead's full name.
  • first_name — lead's first name.
  • last_name — lead's last name.
  • email — lead's email.
  • phone — lead's phone.
  • company — lead's company.
  • role — lead's role.
  • stage — lead's stage.
  • meeting_booked — bool.
  • outreach_disabled — bool.

Conversations

GET/v1/leads/{lead_id}/callslist calls for a lead

Returns the call history for a single lead.

Params

  • limit — max number of results to return.
  • cursor — pagination cursor.

Response — each call has the shape:

{
  "id": "...",
  "lead_id": "...",
  "agent_id": "...",
  "direction": "...",
  "lead_phone": "...",
  "agent_phone": "...",
  "outcome": "connected",
  "duration_seconds": 0,
  "summary": "...",
  "transcript_url": "...",
  "recording_url": "...",
  "started_at": "...",
  "finished_at": "..."
}

outcomeconnected | voicemail | no_answer.

GET/v1/leads/{lead_id}/emailslist emails for a lead

Returns the email history for a single lead.

Params

  • limit — max number of results to return.
  • cursor — pagination cursor.

Response — each email has the shape:

{
  "id": "...",
  "lead_id": "...",
  "agent_id": "...",
  "subject": "...",
  "body": "...",
  "summary": "...",
  "lead_email": "...",
  "status": "...",
  "workflow_id": "...",
  "created_at": "..."
}

GET/v1/calls/{call_id}/audiofetch a call recording

Streams the raw audio bytes of a call's recording (resolve call_id from GET /v1/leads/{lead_id}/calls). Provider auth is handled server-side. The response Content-Type is the audio MIME (e.g. audio/mpeg).

curl -H "Authorization: Bearer $KEY" \
  https://YOUR-APP/api/v1/calls/CALL_ID/audio --output call.mp3

If no recording exists for the call, returns 404 recording_unavailable — this usually means call recording/transcription isn't enabled for the project or agent. (MCP tool: get_call_audio returns the audio inline plus a transcript_url when present.)

GET/v1/activityrecent event feed

Returns a feed of recent events.

Params

  • lead_id — filter by lead.
  • agent_id — filter by agent.
  • event_type — filter by event type.
  • limit — max number of results to return.
  • cursor — pagination cursor.

Response — each entry has the shape:

{
  "id": "...",
  "agent_id": "...",
  "agent_type": "...",
  "event_type": "...",
  "title": "...",
  "detail": "...",
  "status": "...",
  "lead_id": "...",
  "lead_name": "...",
  "metadata": {},
  "created_at": "..."
}

Agents

GET/v1/agentslist

List agents.

Params

  • type — filter by agent type (outbound | inbound).
  • limit — max number of agents to return.
  • cursor — pagination cursor.

Response — each agent has this shape:

{
  "id": "...",
  "type": "...",
  "name": "...",
  "mode": "...",
  "automation_enabled": false,
  "twilio_number": "...",
  "email_address": "...",
  "language": "...",
  "voice_id": "...",
  "elevenlabs_agent_id": "...",
  "created_at": "..."
}

GET/v1/agents/{agent_id}detail + stats

Returns a single agent plus aggregate stats.

Response — same shape as the list item, with these additional fields:

{
  "total_leads_assigned": 0,
  "total_calls": 0,
  "total_emails": 0
}

POST/v1/agentscreate

Create an agent. Voice provisioning is configured in-app; the API creates the agent record with automation off.

Body

  • name — agent name.
  • type — required (outbound | inbound).
  • outreach_model — one of call | email | both.
  • language — agent language.
{
  "name": "Sales Agent",
  "type": "outbound",
  "outreach_model": "both",
  "language": "en"
}

PATCH/v1/agents/{agent_id}update

Update an agent's settings and brain text fields.

Body

  • name — agent name.
  • automation_enabled — bool.
  • outreach_model — one of call | email | both.
  • language — agent language.
  • system_prompt — brain text field.
  • product_explanation — brain text field.
  • value_proposition — brain text field.
  • pain_points — brain text field.
  • benefits — brain text field.
  • features_differentiators — brain text field.
  • elevator_pitch — brain text field.
  • first_message — brain text field.
  • email_knowledge — brain text field.
  • example_email — brain text field.

POST/v1/agents/{agent_id}/assign-leads

Assign leads to an agent.

Body — provide either lead_ids or assign_all_unassigned:

{
  "lead_ids": ["id1", "id2"]
}
{
  "assign_all_unassigned": true
}

Response

{
  "data": {
    "assigned": 0,
    "total": 0
  }
}

Outreach — real side effects

POST/v1/outreach/callplace a real call

Place a real call to a lead. An Idempotency-Key header is required.

Body

  • lead_id — the lead to call (required).
  • agent_id — the agent placing the call (required).
  • call_reason — reason for the call (optional).
  • first_message_override — overrides the agent's first message (optional).
{
  "lead_id": "...",
  "agent_id": "...",
  "call_reason": "optional",
  "first_message_override": "optional"
}
curl -X POST -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"lead_id":"L1","agent_id":"A1","call_reason":"intro"}' \
  https://YOUR-APP/api/v1/outreach/call

Response202

{
  "data": {
    "call_id": "...",
    "task_id": "...",
    "status": "queued"
  }
}

The call is dialed by Cora's automation within ~1 minute.

Errors: no_phone_number (agent has no number), lead_no_phone, lead_stopped (lead opted out), resource_not_found.

POST/v1/outreach/emailsend a real email

Send a real email to a lead.

Body

  • lead_id — the lead to email (required).
  • agent_id — the agent sending the email (required).
  • subject — email subject (optional).
  • message — email body (optional).
  • template — template to use (optional).
{
  "lead_id": "...",
  "agent_id": "...",
  "subject": "optional",
  "message": "optional",
  "template": "optional"
}

Response202

{
  "data": {
    "email_id": "...",
    "task_id": "...",
    "status": "queued"
  }
}

Errors: agent_no_email, lead_no_email, lead_stopped.

Note: subject/message/template are accepted and stored, but the email worker currently composes the message itself — manual overrides take effect only once the email worker is updated to honor them.

Phone numbers

GET/v1/phone-numberslist phone numbers

Returns all phone numbers in the workspace. Not paginated (no limit/cursor/has_more), unlike the other list endpoints.

Response

{
  "data": [
    {
      "id": "...",
      "number": "...",
      "outbound_agent_id": "...",
      "inbound_agent_id": "...",
      "country": "...",
      "friendly_name": "...",
      "status": "...",
      "created_at": "..."
    }
  ]
}

POST/v1/phone-numbers/{phone_id}/assignassign a phone number to an agent

Assigns a phone number to an agent in either an outbound or inbound role.

Body

  • agent_id — the agent to assign the phone number to.
  • role — the assignment role (outbound | inbound).
{
  "agent_id": "...",
  "role": "outbound"
}

Stats

GET/v1/statsworkspace stats

Returns aggregate stats for the workspace.

Params

  • time_range — the time window (7d | 30d | 90d | all; v1 returns all-time).

Response

{
  "total_leads": 0,
  "total_calls": 0,
  "total_emails": 0,
  "meetings_booked": 0,
  "converted": 0,
  "agents": {
    "outbound": 0,
    "inbound": 0
  }
}

API keys (managed in-app)

GET / POST /v1/api-keys and DELETE /v1/api-keys/{key_id} exist for the Cora UI and authenticate with a logged-in Firebase session, not an API key. Create and revoke keys in the dashboard → API tab.


Error codes

codestatusmeaning
missing_api_key / invalid_api_key / revoked_api_key401auth problem with the key
read_only_key403read-only key attempted a write
rate_limit_exceeded429over 120 req/min (see retry_after)
validation_error400bad/missing field (see param)
missing_idempotency_key400outreach/call without Idempotency-Key
resource_not_found404lead/agent/number/key not in your project
idempotency_conflict409same key already in flight
lead_no_phone / lead_no_email422lead lacks the needed contact field
lead_stopped422lead opted out of outreach
no_phone_number / agent_no_email / agent_inactive422agent isn't set up for that channel
recording_unavailable404no recording exists for that call (recording may be off)
internal_error500server error — retry later

Notes & limits (v1)

  • Data is isolated per project — a key only ever sees its own project's data.
  • list_leads is unfiltered full-text on name/email/phone (prefix-ish substring match), not a search index.
  • POST /v1/agents creates the record only; voice/number provisioning is done in-app.
  • Transcripts are returned as transcript_url, not parsed turn-by-turn.