C
conductor
API + MCP REFERENCE

Conductor API docs

Conductor is cold outbound as one API: connect a mailbox (sends go through your own Gmail or Microsoft 365), check and fix deliverability, import and verify contacts, lint the copy, and launch a campaign that is gated on live deliverability. Everything the web app does is available over REST and as a 33-tool MCP server.

The safety model: every send passes the warm-up ramp, daily caps, send windows and suppression list. A bounce spike auto-pauses the campaign. Contact import refuses without a consent attestation, and launch lints every step. These rails are enforced server-side and none of them can be disabled through the API, which is what makes the whole surface safe to hand to an unattended agent.

Quickstart

From nothing to a launched campaign in a few REST calls. Start on a test key: it behaves exactly like live, except provider calls hit a fake mailbox provider (no real mail is ever sent) and usage is never billed. When the flow works, mint a ck_live_ key with the same scopes and swap it in.

0. Mint a test key

In the web app (Settings), or with an existing manage-scope key:

MINT A TEST KEY
curl -s https://api.conductor.fyi/v1/keys \
  -H "Authorization: Bearer $CONDUCTOR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-agent","scopes":["send"],"environment":"test","daily_send_cap":200}'
# -> { ..., "key": "ck_test_..." }   the secret is shown ONCE. Store it.

1. Zero to campaign in 6 calls

One human step happens once: a mailbox is connected via OAuth. Everything else below is an API call. Pacing, warm-up, send windows, the circuit breaker, the compliance footer and suppression enforcement all apply automatically.

REST QUICKSTART · TEST KEY, NO REAL MAIL
API=https://api.conductor.fyi   KEY=ck_test_...   # your test key

# 1) Check the sending domain (free, unmetered). Fix anything failing first.
curl -s $API/v1/check -H "Content-Type: application/json" \
  -d '{"domain":"yourco.com"}'
# -> composite_score, spf, dkim, dmarc, mx, blacklists

# 2) Find your connected mailbox id (one-time human step: OAuth consent
#    in the web app, or GET /v1/mailboxes/oauth/gmail/start for the URL)
curl -s $API/v1/mailboxes -H "Authorization: Bearer $KEY"
# -> [{ "id": "<MAILBOX_ID>", "warmup_state": "warming", "health": ... }]

# 3) Import contacts (consent gate + idempotent retry)
curl -s $API/v1/contacts/import \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "rows": [
      {"email":"jane@acme.com","first_name":"Jane","company":"Acme"},
      {"email":"raj@globex.com","first_name":"Raj","company":"Globex"}
    ],
    "column_map": {"email":"email","first_name":"first_name","company":"company"},
    "list_name": "July prospects",
    "consent_attested": true
  }'
# -> { "imported": 2, "skipped": 0, "suppressed": 0, "list_id": "<LIST_ID>" }
# Verification runs in the background. Invalid contacts are skipped at send time.

# 4) Lint the copy (free). If the score is low, POST /v1/copy/improve.
curl -s $API/v1/copy/lint \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"subject":"Quick question, {{first_name}}","body":"Hi {{first_name}}, saw {{company}} is hiring SDRs. We cut ramp time to 2 weeks for teams like yours. Worth a 15-min chat Thursday?"}'
# -> { "score": 92, "issues": [] }   aim for 80+

# 5) Create the sequence
curl -s $API/v1/sequences \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "SDR intro",
    "status": "active",
    "steps": [
      {"step_order":0,"delay_days":0,"subject_template":"Quick question, {{first_name}}","body_template":"...","stop_on_reply":true},
      {"step_order":1,"delay_days":3,"subject_template":"Re: Quick question, {{first_name}}","body_template":"...","stop_on_reply":true}
    ]
  }'
# -> { "id": "<SEQ_ID>", ... }

# 6) Create the campaign, enroll the list, launch (idempotent)
curl -s $API/v1/campaigns \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name":"July outbound","sequence_id":"<SEQ_ID>","mailbox_ids":["<MAILBOX_ID>"]}'
# -> { "id": "<CAMPAIGN_ID>", "status": "draft" }

curl -s $API/v1/campaigns/<CAMPAIGN_ID>/enroll \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"list_id":"<LIST_ID>"}'
# -> { "enrolled": 2, "skipped_suppressed": 0, "skipped_duplicate": 0 }

curl -s $API/v1/campaigns/<CAMPAIGN_ID>/start \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"copy_quality":"warn"}'
# -> { "status": "active", ... }

2. Watch it run

MONITOR
curl -s $API/v1/campaigns/<CAMPAIGN_ID> -H "Authorization: Bearer $KEY"  # funnel stats
curl -s $API/v1/replies -H "Authorization: Bearer $KEY"                   # unified inbox
curl -s $API/v1/usage -H "Authorization: Bearer $KEY"                     # meters

Handle an unsubscribe the moment you see one: POST /v1/suppressions with {"value":"jane@acme.com","kind":"email","reason":"unsubscribe"}. Suppression halts active enrollments immediately.

Auth + keys

Every authed call sends Authorization: Bearer ck_test_... (or ck_live_...). Keys are minted with POST /v1/keys and revoked instantly with DELETE /v1/keys/:id.

Scopes are hierarchical

manage > send > read. A send key passes read checks; a manage key passes everything. Most agents want ["send"].

Every operation in the OpenAPI spec documents its minimum scope as x-required-scope.

Test vs live

ck_test_ keys exercise the full machine against a fake provider: same code paths, no real mail, usage recorded separately and never billed. Build against test, then swap in ck_live_.

Per-key guardrails: daily_send_cap as a runaway budget, optional expires_at, instant revoke.

Idempotency-Key

Send Idempotency-Key: <uuid> on POST /v1/contacts/import and POST /v1/campaigns/:id/start. A retry with the same key replays the stored response instead of repeating the side effect.

Replays carry an idempotency-replayed: true header. Enrolling the same contact twice is a no-op by design.

REST endpoints

Base URL https://api.conductor.fyi. Wire format is snake_case JSON. The machine-readable version of this table, with every schema, parameter and error, is /v1/openapi.json (OpenAPI 3.1, public).

Scope column = minimum required scope. "public" needs no key (per-IP daily cap applies).

Mailboxes

Connected sending mailboxes. Sends go through your own Gmail or Microsoft 365 account via OAuth; token material is never exposed.

GET/v1/mailboxesreadList connected mailboxes with warm-up state, health and caps
GET/v1/mailboxes/oauth/{provider}/startmanageGet the OAuth consent URL (gmail or m365; a human approves it once)
PATCH/v1/mailboxes/{id}manageUpdate send window, send days, daily cap, from name, signature, timezone
DELETE/v1/mailboxes/{id}manageDisconnect a mailbox (clears tokens, keeps the row for audit)
POST/v1/mailboxes/{id}/test-sendsendSend a test email (free on every plan; fake provider on test keys)

Deliverability

Check a domain, monitor it, and fix what is failing. Checks are free and unmetered. Run one before every launch.

POST/v1/checkpublicRun a deliverability check: SPF, DKIM, DMARC, MX, blacklists + 0-100 score
GET/v1/domainsreadList monitored domains
POST/v1/domainsmanageAdd a domain to monitoring (returns a TXT verification record)
POST/v1/domains/{id}/verifymanageVerify domain ownership via the TXT record
POST/v1/domains/{id}/checkreadRun an on-demand health check on a verified domain
GET/v1/deliverability-statereadOne go / throttle / no_go verdict with reasons, for ?mailbox_id= or ?domain= (the same gate the campaign engine enforces)
GET/v1/domains/{id}/remediation/planreadGet the ordered fix plan (auto_dns items are API-applicable, guided items need a human)
POST/v1/domains/{id}/remediation/connectmanageConnect a Cloudflare API token for auto-apply DNS fixes
POST/v1/domains/{id}/remediation/applymanageApply one auto_dns fix, verify it read-back, re-run the check

Contacts + lists

Import is consent-gated and idempotent. Verification (MX/SMTP probes) runs in the background; invalid contacts are skipped at send time.

POST/v1/contacts/importsendBulk import with a column map (consent_attested must be true; send an Idempotency-Key)
GET/v1/contactsreadList contacts, paginated, filter by list_id (includes verification_status)
POST/v1/contactssendCreate a single contact (upserted by org + email)
POST/v1/contacts/{id}/suppresssendSuppress a contact and halt their active enrollments
GET/v1/listsreadList contact lists
POST/v1/listssendCreate a contact list
GET/v1/lists/{id}readGet a list
DELETE/v1/lists/{id}sendDelete a list (contacts are kept)
POST/v1/lists/{id}/memberssendAdd contacts to a list (idempotent)

Sequences

Multi-step sequences with merge tags: {{first_name}}, {{last_name}}, {{company}}, {{title}}, {{email}} plus any custom fields you imported.

GET/v1/sequencesreadList sequences
POST/v1/sequencessendCreate a sequence with ordered steps (step 0 sends on enroll)
GET/v1/sequences/{id}readGet a sequence with its steps
PATCH/v1/sequences/{id}sendRename, change status, or replace the whole step set
POST/v1/sequences/{id}/activatesendActivate a sequence (needs at least one step)

Copy quality

Lint is deterministic, free and never metered. Improve is an LLM rewrite, metered as copy_improve. Aim for a lint score of 80+.

POST/v1/copy/lintreadScore copy 0-100 and list issues: slop openers, spam triggers, broken merge tags, no clear ask
POST/v1/copy/improvesendRewrite to the house style: short, specific, one ask, no slop (returns before/after scores)

Campaigns

Create a draft, enroll, launch. Warm-up ramp, send windows, suppression checks, the bounce circuit breaker and the copy-quality gate apply on every send.

GET/v1/campaignsreadList campaigns
POST/v1/campaignssendCreate a draft campaign bound to a sequence + mailboxes (safe defaults for the rest)
GET/v1/campaigns/{id}readGet a campaign with funnel stats: enrolled, sent, opened, clicked, replied, bounced, unsubscribed
PATCH/v1/campaigns/{id}sendEdit settings: name, caps, timezone, circuit breaker, mailboxes
DELETE/v1/campaigns/{id}sendDelete a campaign (enrollments and messages cascade)
POST/v1/campaigns/{id}/enrollsendEnroll a list or explicit contact ids (suppressed contacts skipped, re-enroll is a no-op)
POST/v1/campaigns/{id}/startsendLaunch: draft to active (idempotent via Idempotency-Key, copy-quality gated)
POST/v1/campaigns/{id}/pausesendPause: active to paused (resume with start)
POST/v1/campaigns/{id}/stopsendStop: terminal, halts all future sends

Replies

A unified inbox across every campaign. Every inbound is classified automatically (positive, negative, question, ooo_auto_reply, unsubscribe, hard_bounce, soft_bounce); routine classes are handled server-side and hidden by default. Positive replies pause their sequence and get a booking confirmation draft. Prefer webhooks for reply notifications; poll this as the fallback.

GET/v1/repliesreadList replies, newest first (with unread count). Default = real human replies; ?filter=all includes auto-handled traffic, ?classification= pinpoints one class
GET/v1/replies/settingsreadRead the org booking_url and auto_confirm flag
PATCH/v1/replies/settingsmanageSet booking_url (http/https, null clears) and/or auto_confirm
GET/v1/replies/{eventId}/confirmation-draftreadFetch (generating on first call) the booking confirmation draft for a positive reply. 409 booking_url_missing until a booking link is set
POST/v1/replies/{eventId}/confirmation-draft/sendsendSend the confirmation, threaded into the conversation. Idempotent: an already-sent draft replays with sent:false; 502 send_failed is retryable
GET/v1/replies/{enrollmentId}/threadreadFull conversation thread for one enrollment (marks replies read)
POST/v1/replies/{enrollmentId}/replysendSend an outgoing reply on the thread

Suppressions

Enforced on every enroll and every send. Adding one cascades immediately: matching contacts are flagged and active enrollments halted.

GET/v1/suppressionsreadList suppressions (org + platform-global)
POST/v1/suppressionssendSuppress an email address or a whole domain (unsubscribe, bounce, complaint, manual)
DELETE/v1/suppressions/{id}sendRemove an org suppression (global rows are not deletable)

Keys

Scoped API keys. The secret is returned exactly once, at mint time.

GET/v1/keysmanageList API keys (metadata only)
POST/v1/keysmanageMint a key: scopes, live or test, optional daily_send_cap and expires_at
DELETE/v1/keys/{id}manageRevoke a key (kill switch, effective immediately)

Usage

Three meters: email_sent (1,000 free/mo then $0.002), email_verified (500 free/mo then $0.005), copy_improve (100 free/mo then $0.01). Safety features are never metered.

GET/v1/usagereadUsage rollup per meter: used, free_remaining, overage (test usage reported separately, never billed)

Webhooks

HMAC-signed event delivery for replies, bounces, unsubscribes and campaign state. Max 10 endpoints per org.

GET/v1/webhooksmanageList webhook endpoints
POST/v1/webhooksmanageRegister an endpoint (whsec_ signing secret shown once)
DELETE/v1/webhooks/{id}manageDelete an endpoint

MCP server

The Conductor MCP server gives Claude Code, Cursor, Claude Desktop, or any MCP client the whole machine as 33 tools. It runs over stdio, wraps the REST API with safe defaults, and sends idempotency keys automatically so agent retries never double-launch or double-import. Nothing in the server can turn a safety gate off. Set CONDUCTOR_API_KEY (start with a test key) and optionally CONDUCTOR_API_BASE (defaults to https://api.conductor.fyi).

Claude Code

ONE COMMAND
claude mcp add conductor \
  --env CONDUCTOR_API_KEY=ck_test_your_key_here \
  -- node apps/mcp/dist/index.js

The server ships in the Conductor repo at apps/mcp. Build it once with pnpm --filter @deliverly/mcp build, then point your client at dist/index.js.

Cursor + Claude Desktop

.cursor/mcp.json · claude_desktop_config.json · .mcp.json
{
  "mcpServers": {
    "conductor": {
      "command": "node",
      "args": ["/absolute/path/to/apps/mcp/dist/index.js"],
      "env": {
        "CONDUCTOR_API_KEY": "ck_test_your_key_here",
        "CONDUCTOR_API_BASE": "https://api.conductor.fyi"
      }
    }
  }
}

The same JSON block works in Cursor (.cursor/mcp.json), Claude Desktop (Settings, Developer, Edit Config) and Claude Code's .mcp.json.

The 33 tools

connect_mailboxReturns the OAuth consent URL (a human approves it once)
get_mailbox_statusMailboxes with warm-up state, health and caps
check_deliverabilitySPF/DKIM/DMARC/MX/blacklists + 0-100 score (free)
get_deliverability_stateOne go / throttle / no_go verdict with reasons; the same gate the engine enforces at send time (free)
fix_deliverabilityRemediation plan; optionally auto-apply DNS fixes
connect_dnsConnect a zone-scoped Cloudflare token so DNS fixes can auto-apply
import_contactsBulk import (consent-gated, idempotent, auto-verifies)
verify_contactsVerification results for a list: valid, invalid, pending
create_sequenceMulti-step sequence with merge tags
lint_copy0-100 anti-slop and spam score plus issues (free)
improve_copyLLM rewrite to the house style (metered)
launch_campaignMailbox + contacts + sequence and go; safe defaults for the rest
pause_campaignPause an active campaign (resumable)
stop_campaignStop a campaign permanently (terminal)
get_campaign_statsFunnel: sent, opened, clicked, replied, bounced
list_repliesUnified inbox: real human replies by default, auto-handled traffic hidden
triage_repliesReplies by intent (positive, negative, question, and more) with per-class counts
get_confirmation_draftBooking confirmation draft for a positive reply (idempotent; nothing is sent)
send_confirmationSend a reviewed confirmation draft through the connected mailbox
list_meetingsMeetings booked through native reply-to-book, on the connected mailbox calendar
suppressSuppress an email or a whole domain, effective immediately
get_usageMetered usage plus free allowances for the period

The typical flow: check_deliverability, import_contacts, lint_copy, create_sequence, launch_campaign, get_campaign_stats. launch_campaign needs only mailbox + contacts + sequence; everything else defaults server-side. When replies land: triage_replies, then get_confirmation_draft and send_confirmation on the positive ones.

Errors, rate limits, webhooks

Contracts your retry logic can rely on. Retry only when retryable is true; other 4xx mean fix the request, not retry it.

EVERY ERROR, ONE SHAPE
{ "error": "invalid_input", "detail": "domain is required", "retryable": false }

# error      stable snake_case code, safe to branch on
#            e.g. invalid_input, unauthorized, insufficient_scope,
#            not_found, consent_required, copy_quality_blocked,
#            upgrade_required, send_failed
# detail     human-readable context (string or structured field errors)
# retryable  true only when the identical request may succeed later
#            (429, 5xx, temporary degradation). Other 4xx: fix, do not retry.
WEBHOOK DELIVERY HEADERS
X-Conductor-Event: message.replied
X-Conductor-Signature: t=<unix-seconds>,v1=<hex hmac-sha256 of "<t>.<body>">
# signed with the whsec_ secret returned once at creation
# endpoints auto-disable after 8 consecutive delivery failures

Rate limits

100 requests/min per org. A 429 sets retryable: true; back off and retry. The public /v1/check endpoint has a per-IP daily cap; use an API key for more.

Send caps

Independent of request limits, sends respect the mailbox warm-up ramp, daily caps, send windows and send days, plus any per-key daily_send_cap. A refused send returns a 409 carrying the gate code.

Webhook events

message.sent, message.opened, message.clicked, message.replied, message.bounced, contact.unsubscribed, campaign.paused, campaign.completed. Polling fallback: GET /v1/replies.

The machine-readable spec

Everything on this page, plus every schema, parameter, scope and error, is published as OpenAPI 3.1 at api.conductor.fyi/v1/openapi.json. It is public, so an agent can discover the machine before it holds a key.