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:
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.
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
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" # metersHandle 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/mailboxes | read | List connected mailboxes with warm-up state, health and caps |
| GET | /v1/mailboxes/oauth/{provider}/start | manage | Get the OAuth consent URL (gmail or m365; a human approves it once) |
| PATCH | /v1/mailboxes/{id} | manage | Update send window, send days, daily cap, from name, signature, timezone |
| DELETE | /v1/mailboxes/{id} | manage | Disconnect a mailbox (clears tokens, keeps the row for audit) |
| POST | /v1/mailboxes/{id}/test-send | send | Send 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/check | public | Run a deliverability check: SPF, DKIM, DMARC, MX, blacklists + 0-100 score |
| GET | /v1/domains | read | List monitored domains |
| POST | /v1/domains | manage | Add a domain to monitoring (returns a TXT verification record) |
| POST | /v1/domains/{id}/verify | manage | Verify domain ownership via the TXT record |
| POST | /v1/domains/{id}/check | read | Run an on-demand health check on a verified domain |
| GET | /v1/deliverability-state | read | One go / throttle / no_go verdict with reasons, for ?mailbox_id= or ?domain= (the same gate the campaign engine enforces) |
| GET | /v1/domains/{id}/remediation/plan | read | Get the ordered fix plan (auto_dns items are API-applicable, guided items need a human) |
| POST | /v1/domains/{id}/remediation/connect | manage | Connect a Cloudflare API token for auto-apply DNS fixes |
| POST | /v1/domains/{id}/remediation/apply | manage | Apply 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/import | send | Bulk import with a column map (consent_attested must be true; send an Idempotency-Key) |
| GET | /v1/contacts | read | List contacts, paginated, filter by list_id (includes verification_status) |
| POST | /v1/contacts | send | Create a single contact (upserted by org + email) |
| POST | /v1/contacts/{id}/suppress | send | Suppress a contact and halt their active enrollments |
| GET | /v1/lists | read | List contact lists |
| POST | /v1/lists | send | Create a contact list |
| GET | /v1/lists/{id} | read | Get a list |
| DELETE | /v1/lists/{id} | send | Delete a list (contacts are kept) |
| POST | /v1/lists/{id}/members | send | Add 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/sequences | read | List sequences |
| POST | /v1/sequences | send | Create a sequence with ordered steps (step 0 sends on enroll) |
| GET | /v1/sequences/{id} | read | Get a sequence with its steps |
| PATCH | /v1/sequences/{id} | send | Rename, change status, or replace the whole step set |
| POST | /v1/sequences/{id}/activate | send | Activate 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/lint | read | Score copy 0-100 and list issues: slop openers, spam triggers, broken merge tags, no clear ask |
| POST | /v1/copy/improve | send | Rewrite 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/campaigns | read | List campaigns |
| POST | /v1/campaigns | send | Create a draft campaign bound to a sequence + mailboxes (safe defaults for the rest) |
| GET | /v1/campaigns/{id} | read | Get a campaign with funnel stats: enrolled, sent, opened, clicked, replied, bounced, unsubscribed |
| PATCH | /v1/campaigns/{id} | send | Edit settings: name, caps, timezone, circuit breaker, mailboxes |
| DELETE | /v1/campaigns/{id} | send | Delete a campaign (enrollments and messages cascade) |
| POST | /v1/campaigns/{id}/enroll | send | Enroll a list or explicit contact ids (suppressed contacts skipped, re-enroll is a no-op) |
| POST | /v1/campaigns/{id}/start | send | Launch: draft to active (idempotent via Idempotency-Key, copy-quality gated) |
| POST | /v1/campaigns/{id}/pause | send | Pause: active to paused (resume with start) |
| POST | /v1/campaigns/{id}/stop | send | Stop: 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/replies | read | List replies, newest first (with unread count). Default = real human replies; ?filter=all includes auto-handled traffic, ?classification= pinpoints one class |
| GET | /v1/replies/settings | read | Read the org booking_url and auto_confirm flag |
| PATCH | /v1/replies/settings | manage | Set booking_url (http/https, null clears) and/or auto_confirm |
| GET | /v1/replies/{eventId}/confirmation-draft | read | Fetch (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/send | send | Send the confirmation, threaded into the conversation. Idempotent: an already-sent draft replays with sent:false; 502 send_failed is retryable |
| GET | /v1/replies/{enrollmentId}/thread | read | Full conversation thread for one enrollment (marks replies read) |
| POST | /v1/replies/{enrollmentId}/reply | send | Send 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/suppressions | read | List suppressions (org + platform-global) |
| POST | /v1/suppressions | send | Suppress an email address or a whole domain (unsubscribe, bounce, complaint, manual) |
| DELETE | /v1/suppressions/{id} | send | Remove an org suppression (global rows are not deletable) |
Keys
Scoped API keys. The secret is returned exactly once, at mint time.
| GET | /v1/keys | manage | List API keys (metadata only) |
| POST | /v1/keys | manage | Mint a key: scopes, live or test, optional daily_send_cap and expires_at |
| DELETE | /v1/keys/{id} | manage | Revoke 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/usage | read | Usage 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/webhooks | manage | List webhook endpoints |
| POST | /v1/webhooks | manage | Register an endpoint (whsec_ signing secret shown once) |
| DELETE | /v1/webhooks/{id} | manage | Delete 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
claude mcp add conductor \
--env CONDUCTOR_API_KEY=ck_test_your_key_here \
-- node apps/mcp/dist/index.jsThe 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
{
"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_mailbox | Returns the OAuth consent URL (a human approves it once) |
| get_mailbox_status | Mailboxes with warm-up state, health and caps |
| check_deliverability | SPF/DKIM/DMARC/MX/blacklists + 0-100 score (free) |
| get_deliverability_state | One go / throttle / no_go verdict with reasons; the same gate the engine enforces at send time (free) |
| fix_deliverability | Remediation plan; optionally auto-apply DNS fixes |
| connect_dns | Connect a zone-scoped Cloudflare token so DNS fixes can auto-apply |
| import_contacts | Bulk import (consent-gated, idempotent, auto-verifies) |
| verify_contacts | Verification results for a list: valid, invalid, pending |
| create_sequence | Multi-step sequence with merge tags |
| lint_copy | 0-100 anti-slop and spam score plus issues (free) |
| improve_copy | LLM rewrite to the house style (metered) |
| launch_campaign | Mailbox + contacts + sequence and go; safe defaults for the rest |
| pause_campaign | Pause an active campaign (resumable) |
| stop_campaign | Stop a campaign permanently (terminal) |
| get_campaign_stats | Funnel: sent, opened, clicked, replied, bounced |
| list_replies | Unified inbox: real human replies by default, auto-handled traffic hidden |
| triage_replies | Replies by intent (positive, negative, question, and more) with per-class counts |
| get_confirmation_draft | Booking confirmation draft for a positive reply (idempotent; nothing is sent) |
| send_confirmation | Send a reviewed confirmation draft through the connected mailbox |
| list_meetings | Meetings booked through native reply-to-book, on the connected mailbox calendar |
| suppress | Suppress an email or a whole domain, effective immediately |
| get_usage | Metered 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.
{ "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.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 failuresRate 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.