Cora API & MCP Server — v1
Cora is an AI SDR — an AI workforce for B2B outbound sales that autonomously calls and emails your leads to book meetings. The Cora API is an agent-first REST API, and the Cora MCP server exposes the same operations as 24 tools, so any AI agent (Claude, Claude Code, Cursor) can drive Cora directly. Authenticate REST with an API key (cora_live_...); authenticate the MCP connector with OAuth — no API key needed.
- Base URL:
https://www.cora-intelligence.com/api/v1— Cora's hosted API (use your own host if self-hosting). Local dev:http://localhost:3000/api/v1. - Full URL = Base URL + the path in each heading. The MCP connector lives at
https://www.cora-intelligence.com/api/mcp(under/api, not/api/v1). - Auth:
Authorization: Bearer cora_live_...for REST/CLI · OAuth for the MCP connector (no key). - Content-Type:
application/jsonfor request bodies. - This doc (markdown):
GET /api/v1/docs· Machine-readable index:GET /api/v1
Quick facts
- Free to build, paid to send. Creating agents, leads, and config is free; sending real calls/emails returns
402 payment_requireduntil you subscribe. - The MCP connector uses OAuth — no API key required. API keys are only for the CLI and direct REST.
- 24 MCP tools, one per REST operation (one exception, noted below).
- Rate limit: 120 requests/minute per key.
- Self-serve signup, no sales call —
POST /v1/signupreturns an API key instantly.
What you can do
The full Cora API surface — each capability with its REST endpoint and MCP tool. Every REST operation maps 1:1 to an MCP tool, with one exception (billing portal).
| Capability | REST endpoint | MCP tool |
|---|---|---|
| List / create projects (workspaces) | GET / POST /v1/projects | list_projects, create_project |
| Check remaining credits | GET /v1/credits | get_credits |
| List / search leads | GET /v1/leads | list_leads |
| Get full lead detail | GET /v1/leads/{id} | get_lead |
| Create a lead | POST /v1/leads | create_lead |
| Bulk-create leads (≤ 500) | POST /v1/leads/bulk | create_leads_bulk |
| Update a lead | PATCH /v1/leads/{id} | update_lead |
| Read a lead's calls / emails | GET /v1/leads/{id}/calls, /emails | get_lead_calls, get_lead_emails |
| Download a call recording | GET /v1/calls/{id}/audio | get_call_audio |
| Recent activity feed | GET /v1/activity | get_activity |
| List / get / create / update agents | GET / POST /v1/agents, PATCH /v1/agents/{id} | list_agents, get_agent, create_agent, update_agent |
| Assign leads to an agent | POST /v1/agents/{id}/assign-leads | assign_leads_to_agent |
| Place a real phone call | POST /v1/outreach/call | trigger_call |
| Send a real email | POST /v1/outreach/email | send_email |
| List phone numbers | GET /v1/phone-numbers | list_phone_numbers |
| Assign a phone number to an agent | POST /v1/phone-numbers/{id}/assign | assign_phone_number |
| Workspace metrics | GET /v1/stats | get_stats |
| List plans / start subscription checkout | GET /v1/billing/plans, POST /v1/billing/checkout | list_plans, create_checkout |
| Open the billing portal | POST /v1/billing/portal | — (REST only, no MCP tool) |
That's the complete catalog of 24 MCP tools. POST /v1/billing/portal is the one REST endpoint without a matching tool.
Some MCP tool args differ from the REST field names: assign_phone_number takes agent_type (REST body uses role); get_activity takes type (REST query uses event_type). Tool names also differ from REST verbs — e.g. POST /v1/outreach/call → trigger_call, GET /v1/leads/{id}/calls → get_lead_calls.
FAQ
Does Cora have an API? Yes. Cora has an agent-first REST API at https://www.cora-intelligence.com/api/v1. Create an account and API key with no human steps (POST /v1/signup), then send Authorization: Bearer cora_live_... on every request.
Does Cora have an MCP server? Yes. The Cora MCP server is at https://www.cora-intelligence.com/api/mcp and exposes every API operation as a tool (24 tools). Add it to Claude, Claude Code, or Cursor as a custom connector.
Do I need an API key for the MCP connector? No. The connector uses OAuth 2.1 with Dynamic Client Registration — you sign in and approve once. The cora_live_... API key is only for the CLI and direct REST calls.
Is the Cora API free? Building is free — creating agents, leads, and configuration costs nothing. Sending real calls and emails needs an active subscription; until then, outreach returns 402 payment_required.
How do I get a Cora API key? Self-serve: POST /v1/signup with your email (no human steps), or use the form at https://www.cora-intelligence.com/for-agents. You can also create and revoke keys in the Cora dashboard → API tab.
What is Cora? Cora is an AI SDR (AI workforce) for B2B outbound sales: it sources and qualifies leads, then calls and emails them on your behalf to book meetings.
Connect to Claude / Cursor (MCP) — recommended for AI agents
If you're an AI agent (Claude, Claude Cowork, Cursor, …), connect through the Cora MCP server, not the raw REST API. The connector uses OAuth — you do NOT need an API key for it. Every operation in this reference is exposed as an MCP tool.
Connector URL: https://www.cora-intelligence.com/api/mcp
Three steps:
- Add Cora as a custom connector with the URL above.
- Claude Code:
claude mcp add --transport http cora https://www.cora-intelligence.com/api/mcp - Claude Desktop / Cowork / Cursor: Settings → Connectors → Add custom connector → paste the URL.
- Claude Code:
- Leave the "Advanced settings" empty — do not fill in an OAuth Client ID or Secret. The server supports Dynamic Client Registration (RFC 7591), so the client registers itself automatically.
- A browser opens → sign in with Cora and approve → the tools light up across all your projects.
OAuth details: OAuth 2.1 with PKCE (S256) + Dynamic Client Registration; scopes cora.read and cora.write (a read-only consent grants cora.read only). Discovery: GET /.well-known/oauth-authorization-server and GET /.well-known/oauth-protected-resource.
API key vs. connector — don't mix them up: the cora_live_… API key is only for the CLI and direct REST calls (the Authorization: Bearer header below). It is not used by the Claude/Cursor connector — that's pure OAuth. MCP tool names match the operations here (e.g. list_leads, create_lead, trigger_call, send_email, get_call_audio, get_stats).
Prefer direct REST or the CLI? Provision an account and API key below.
Sign up & go live
You can self-provision an account and API key with no human steps — no sales call, no manual approval.
1. Create an account — POST /v1/signup (no auth, rate-limited)
Body:
{ "email": "agent@example.com", "password": "optional", "company_name": "Acme", "name": "Jane", "source": "optional" }
Only email is required. (Signup is gated server-side by CORA_AGENT_SIGNUP_ENABLED → 403 signup_disabled when off.) Returns 201:
{
"user": { "id": "...", "email": "...", "name": "..." },
"organization": { "id": "..." },
"project": { "id": "...", "name": "..." },
"api_key": "cora_live_...",
"password": "generated — shown ONCE if you didn't supply one",
"api_base_url": "https://www.cora-intelligence.com/api/v1",
"docs_url": "https://www.cora-intelligence.com/api/v1/docs",
"plans": [ ... ],
"checkout_endpoint": "/v1/billing/checkout",
"verify_email_url": "...",
"next_steps": [ ... ]
}
project.id is the project_id you pass to MCP tools. If you didn't send a password, the generated one is returned once — capture it for dashboard login.
curl -X POST -H "Content-Type: application/json" \
-d '{"email":"agent@example.com","company_name":"Acme"}' \
https://www.cora-intelligence.com/api/v1/signup
Save the api_key — it's shown once. Use it as Authorization: Bearer cora_live_... on every other endpoint.
2. Build for free, pay to send
Creating agents, leads, and configuration is free. Sending real outreach (calls/emails via POST /v1/outreach/*) returns 402 payment_required until your project has an active subscription. Build and test the whole setup, then subscribe when you're ready to go live.
3. Subscribe — pick a plan and check out
GET /v1/billing/plans— list available subscription plans.POST /v1/billing/checkoutwith{ "plan": "starter|growth|pro", "interval": "monthly|annual" }— returns{ "checkout_url": "..." }.- Open
checkout_url(Stripe) to complete payment. Manage billing later viaPOST /v1/billing/portal.
Once subscribed, the 402 goes away and POST /v1/outreach/call / POST /v1/outreach/email place real calls and send real emails.
4. Connect via MCP (optional)
Prefer a connector over raw REST? Point an MCP client at https://www.cora-intelligence.com/api/mcp. Two ways to authenticate:
- API key (headless): send
Authorization: Bearer cora_live_...(the key from/v1/signup). No consent screen — ideal for autonomous agents. Tools are scoped to that key's project. - OAuth (one approval): connectors that don't take a static header start the OAuth flow — a human signs in and approves once.
Almost every REST endpoint maps 1:1 to an MCP tool — including billing: list_plans and create_checkout let an agent fetch plans and produce the Stripe subscription link directly through the connector. The one exception is POST /v1/billing/portal, which has no MCP tool (it's REST-only).
Quickstart — first real call
POST /v1/signup→ save theapi_keyandproject.id.POST /v1/agents→ create anoutboundagent.GET /v1/phone-numbers, thenPOST /v1/phone-numbers/{id}/assign→ give the agent a number. An agent must have a phone number before it can call (elseno_phone_number).POST /v1/leads→ create a lead with aphonein E.164 format.POST /v1/agents/{id}/assign-leads→ assign the lead to the agent.POST /v1/billing/checkout→ subscribe (real outreach needs an active plan), thenPOST /v1/outreach/callwith anIdempotency-Keyplaces the call.
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.
Data model: an organization (your company) contains projects (workspaces); each project holds agents, leads, and phone numbers. Scoping: a REST API key is scoped to a single project. The MCP/OAuth connection is user-scoped — most tools take an optional project_id (auto-selected when you have one project; otherwise call list_projects and pass it).
Typical flows:
- "What's happening?" →
GET /v1/statsfor totals,GET /v1/activityfor the recent event feed. - "Find a lead" →
GET /v1/leads?query=jane. ThenGET /v1/leads/{id}for full detail incl. recent calls, emails, and activity. - "Add leads" →
POST /v1/leads(one) orPOST /v1/leads/bulk(up to 500; duplicates by email are skipped). - "Reach out" →
POST /v1/outreach/callorPOST /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}, thenPOST /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://www.cora-intelligence.com/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.
| Failure | Status | code |
|---|---|---|
No Authorization header | 401 | missing_api_key |
| Unknown / malformed key | 401 | invalid_api_key |
| Revoked key | 401 | revoked_api_key |
| read_only key on a write | 403 | read_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-LimitX-RateLimit-RemainingX-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
Projects
GET/v1/projectslist projects
List the projects you can access (owned + your company's). MCP tool: list_projects.
Response
{ "data": [ { "id": "...", "name": "...", "org_id": "..." } ] }
POST/v1/projectscreate a project
Create a new project (workspace) in your company. Use the returned id as project_id on the other tools to build the rest of the flow (leads → agent → calls). MCP tool: create_project.
Body
name— the project name.
Response
{ "data": { "id": "...", "name": "...", "org_id": "..." } }
Account
GET/v1/creditsremaining credits
Show the credits left on your account (credits fund phone numbers, calls, and outreach). MCP tool: get_credits.
Response
{ "data": { "credits_remaining": 500, "unit": "credits" } }
Leads
GET/v1/leadslist / search
List or search leads.
Params
query— matches name/email/phone.status— stage filter:new|in_progress|converted.agent_id— filter by assigned agent.has_meeting—true/false.limit— page size.cursor— pagination cursor.
curl -H "Authorization: Bearer $KEY" "https://www.cora-intelligence.com/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. MCP tool: get_lead_calls.
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": "..."
}
outcome ∈ connected | voicemail | no_answer.
GET/v1/leads/{lead_id}/emailslist emails for a lead
Returns the email history for a single lead. MCP tool: get_lead_emails.
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://www.cora-intelligence.com/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 does not return inline audio — it returns a short-lived signed download_url (expires in ~30 min) plus a transcript_url when present, to stay within the MCP tool-result token budget. This REST endpoint streams the raw bytes.)
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 ofcall|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 ofcall|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
Outreach is asynchronous: /outreach/call and /outreach/email return 202 with a call_id/email_id + task_id and status: "queued". There are no outbound webhooks — to learn the outcome, poll GET /v1/leads/{id}/calls (or /emails) and GET /v1/activity.
POST/v1/outreach/callplace a real call
Place a real call to a lead. An Idempotency-Key header is required. MCP tool: trigger_call.
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://www.cora-intelligence.com/api/v1/outreach/call
Response — 202
{
"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. MCP tool: send_email.
Body
lead_id— the lead to email (required).agent_id— the agent sending the email (required).subject— email subject (optional; currently ignored — the agent composes the email).message— email body (optional; currently ignored).template— template to use (optional; currently ignored).
{
"lead_id": "...",
"agent_id": "...",
"subject": "optional",
"message": "optional",
"template": "optional"
}
Response — 202
{
"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). Accepted but ignored in v1 — always 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
| code | status | meaning |
|---|---|---|
missing_api_key / invalid_api_key / revoked_api_key | 401 | auth problem with the key |
read_only_key | 403 | read-only key attempted a write |
signup_disabled | 403 | agent signup is disabled on this deployment |
rate_limit_exceeded | 429 | over 120 req/min (see retry_after) |
validation_error | 400 | bad/missing field (see param) |
missing_idempotency_key | 400 | outreach/call without Idempotency-Key |
resource_not_found | 404 | lead/agent/number/key not in your project |
idempotency_conflict | 409 | same key already in flight |
lead_no_phone / lead_no_email | 422 | lead lacks the needed contact field |
lead_stopped | 422 | lead opted out of outreach |
no_phone_number / agent_no_email / agent_inactive | 422 | agent isn't set up for that channel |
recording_unavailable | 404 | no recording exists for that call (recording may be off) |
internal_error | 500 | server error — retry later |
Notes & limits (v1)
- Data is isolated per project — a key only ever sees its own project's data.
list_leadsis unfiltered full-text on name/email/phone (prefix-ish substring match), not a search index.POST /v1/agentscreates the record only; voice/number provisioning is done in-app.- Transcripts are returned as
transcript_url, not parsed turn-by-turn. v1is the stable version; breaking changes ship under a new version path.- No outbound webhooks and no official SDKs yet. The CLI + local (stdio) MCP server ship as the npm package
@cora-intelligence/cli(npx -y @cora-intelligence/cli cora-mcp, envCORA_API_KEY). GET /api/v1is the machine-readable endpoint index (there's no OpenAPI spec yet).