[ Cora Intelligence ]

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/json for 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_required until 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 callPOST /v1/signup returns 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).

CapabilityREST endpointMCP tool
List / create projects (workspaces)GET / POST /v1/projectslist_projects, create_project
Check remaining creditsGET /v1/creditsget_credits
List / search leadsGET /v1/leadslist_leads
Get full lead detailGET /v1/leads/{id}get_lead
Create a leadPOST /v1/leadscreate_lead
Bulk-create leads (≤ 500)POST /v1/leads/bulkcreate_leads_bulk
Update a leadPATCH /v1/leads/{id}update_lead
Read a lead's calls / emailsGET /v1/leads/{id}/calls, /emailsget_lead_calls, get_lead_emails
Download a call recordingGET /v1/calls/{id}/audioget_call_audio
Recent activity feedGET /v1/activityget_activity
List / get / create / update agentsGET / POST /v1/agents, PATCH /v1/agents/{id}list_agents, get_agent, create_agent, update_agent
Assign leads to an agentPOST /v1/agents/{id}/assign-leadsassign_leads_to_agent
Place a real phone callPOST /v1/outreach/calltrigger_call
Send a real emailPOST /v1/outreach/emailsend_email
List phone numbersGET /v1/phone-numberslist_phone_numbers
Assign a phone number to an agentPOST /v1/phone-numbers/{id}/assignassign_phone_number
Workspace metricsGET /v1/statsget_stats
List plans / start subscription checkoutGET /v1/billing/plans, POST /v1/billing/checkoutlist_plans, create_checkout
Open the billing portalPOST /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/calltrigger_call, GET /v1/leads/{id}/callsget_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.


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:

  1. 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.
  2. 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.
  3. 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_ENABLED403 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

  1. GET /v1/billing/plans — list available subscription plans.
  2. POST /v1/billing/checkout with { "plan": "starter|growth|pro", "interval": "monthly|annual" } — returns { "checkout_url": "..." }.
  3. Open checkout_url (Stripe) to complete payment. Manage billing later via POST /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

  1. POST /v1/signup → save the api_key and project.id.
  2. POST /v1/agents → create an outbound agent.
  3. GET /v1/phone-numbers, then POST /v1/phone-numbers/{id}/assign → give the agent a number. An agent must have a phone number before it can call (else no_phone_number).
  4. POST /v1/leads → create a lead with a phone in E.164 format.
  5. POST /v1/agents/{id}/assign-leads → assign the lead to the agent.
  6. POST /v1/billing/checkout → subscribe (real outreach needs an active plan), then POST /v1/outreach/call with an Idempotency-Key places 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/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://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.

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

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

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://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": "..."
}

outcomeconnected | 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 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

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

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. 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"
}

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

codestatusmeaning
missing_api_key / invalid_api_key / revoked_api_key401auth problem with the key
read_only_key403read-only key attempted a write
signup_disabled403agent signup is disabled on this deployment
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.
  • v1 is 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, env CORA_API_KEY).
  • GET /api/v1 is the machine-readable endpoint index (there's no OpenAPI spec yet).