Authentication
Owners and admins create API keys in Settings → Developers. A key is shown once; only its hash is stored. Send it as a bearer token:
curl https://outember.io/v1/campaigns \
-H "Authorization: Bearer oa_live_…"- A key belongs to one workspace and acts as a member of it: it can import leads, create and run campaigns, read the inbox and reply. It can't manage the team, billing, integrations or other keys.
- Rate limit: 600 requests per minute per key.
- Revoking a key stops it within a minute.
- Your exact API address is shown in Settings → Developers.
Errors
Errors always have the same shape, and message is written for people, so you can show it as is. fields names the inputs that need fixing.
{ "error": { "code": "VALIDATION", "message": "Name your campaign", "fields": { "name": "Name your campaign" } } }Lists are paginated with a cursor: pass the nextCursor you received back as ?cursor=.
Main endpoints
| What | Call |
|---|---|
| Create a lead list | POST /v1/lists { "name": "Q4 founders" } |
| Add leads (up to 1,000 per call) | POST /v1/lists/:id/leads { "rows": [{ "profileUrl": "…", "firstName": "Sara", "company": "Acme", "email": "…" }] } |
| Import a LinkedIn or Sales Navigator search | POST /v1/lists/:id/import-url { "url": "…", "accountId": "…", "maxLeads": 500 } |
| Import from any lead source | POST /v1/lists/:id/import-source with kind: search_url, post_engagers, followers, connections or invitations_received |
| Keep a list updated on a schedule | POST /v1/lists/:id/auto-imports (same body + intervalHours); PATCH/DELETE /v1/auto-imports/:id; run now with POST /v1/auto-imports/:id/run |
| Find emails for a list | POST /v1/lists/:id/find-emails |
| All leads, by search or tag | GET /v1/leads?q=acme&tag=Dubai · GET /v1/leads/tags · GET /v1/leads.csv |
| One lead with campaigns, conversations and notes | GET /v1/leads/:id; edit with PATCH /v1/leads/:id; notes POST /v1/leads/:id/notes |
| Bulk tag, do-not-contact, remove from list | POST /v1/leads/bulk { "leadIds": […], "addTags": ["Q4"], "addToDnc": true } |
| Do-not-contact list | GET /v1/dnc · POST /v1/dnc { "entries": ["ceo@acme.com", "acme.com"] } · DELETE /v1/dnc/:id · GET /v1/dnc.csv |
| Campaigns with funnel stats | GET /v1/campaigns |
| Add a list to a campaign | POST /v1/campaigns/:id/leads { "listId": "…" } |
| Launch, pause, resume | POST /v1/campaigns/:id/launch / pause / resume |
| Leads in a campaign | GET /v1/campaigns/:id/leads?status=replied&q=sara · GET /v1/campaigns/:id/leads.csv |
| Per-step results | GET /v1/campaigns/:id/steps |
| Put a lead back in the sequence | POST /v1/campaigns/:id/leads/:campaignLeadId/resume |
| Conversations | GET /v1/inbox/conversations?intent=interested |
| Messages in a conversation | GET /v1/inbox/conversations/:id/messages |
| Reply (from the account the lead talked to) | POST /v1/inbox/conversations/:id/messages { "text": "…" }, with up to 5 attachments of 8 MB |
| Start a conversation | POST /v1/inbox/conversations { "channel": "linkedin", "accountId": "…", "profileUrl": "…", "text": "…" } |
| Schedule a message | POST /v1/inbox/conversations/:id/scheduled { "text": "…", "sendAt": "2026-11-12T09:00:00Z", "cancelIfReply": true }; cancel with DELETE /v1/inbox/scheduled/:id |
| AI drafts waiting for approval | GET /v1/review; approve with POST /v1/review/:id/approve |
| Withdraw old invitations | POST /v1/accounts/:id/withdraw-invites { "olderThanDays": 30 } |
| Check a sending domain | POST /v1/mailboxes/domain-check { "domain": "acme.com" } |
| Saved sequence templates | GET/POST /v1/templates, PATCH/DELETE /v1/templates/:id |
| Monthly report (JSON or CSV) | GET /v1/reports/monthly?month=2026-11 · GET /v1/reports/monthly.csv?month=2026-11 |
| Team leaderboard | GET /v1/team/leaderboard?days=30 · GET /v1/team/leaderboard.csv?days=30 |
Webhooks
Add an endpoint in Settings → Developers → Webhooks and choose the events. Each delivery is an HTTP POST with a JSON body:
{
"id": "0193…",
"type": "reply.classified",
"occurredAt": "2026-11-09T10:31:00.000Z",
"workspaceId": "…",
"lead": { "id": "…", "firstName": "Sara", "lastName": "Khan", "company": "Acme", "title": "Head of Growth",
"headline": "…", "location": "Dubai", "email": "sara@acme.io", "profileUrl": "https://www.linkedin.com/in/…" },
"campaign": { "id": "…", "name": "Q4 founders" },
"data": { "intent": "interested", "confidence": 0.92, "text": "Sounds good, what times work?", "conversationId": "…" }
}| Event | When | data |
|---|---|---|
connection.accepted | A lead accepted the connection request | — |
reply.received | A lead replied on LinkedIn or by email | channel, text, automatic, conversationId |
reply.classified | The reply assistant classified a reply | intent, confidence, text, conversationId |
lead.finished | A lead left the sequence | status (completed, not_accepted, failed), reason |
email.found | The email finder found an address | email, source, verified |
Verify the signature
Every delivery carries three headers: X-Outreach-Event (the event type), X-Outreach-Delivery (the delivery id, the same on retries) and X-Outreach-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with the endpoint's signing secret (shown once when you create it). Check it before trusting the body:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header, toleranceSeconds = 300) {
const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Delivery rules
- Answer with any 2xx within 15 seconds. Redirects aren't followed.
- Failures are retried about 8 times over roughly 2 hours, with backoff. A 4xx other than 408 and 429 isn't retried.
- Every attempt is logged in Settings → Developers (status, response code, error), and you can retry a delivery from there.
- Each event is delivered once per endpoint. Use
idto make your handler idempotent anyway. - In production, endpoints must use https and resolve to a public address.
REST hooks (Zapier, Make, n8n)
Our Zapier, Make and n8n apps subscribe to events with REST hooks and an API key. Marketplace listings are coming soon; until then, any tool can use the same calls:
| What | Call |
|---|---|
| Check the key (and its workspace) | GET /v1/hooks/me |
| Subscribe | POST /v1/hooks { "url": "https://hooks.zapier.com/…", "event": "reply.classified", "source": "zapier" } → { "id": "…" } |
| Unsubscribe | DELETE /v1/hooks/:id |
| Sample payloads | GET /v1/hooks/sample?event=reply.classified |
| Active subscriptions | GET /v1/hooks |
A subscription is a webhook endpoint like any other: same payloads, signature, retries and delivery log, labelled with its source.
AI assistants (MCP)
The API serves a Model Context Protocol server over Streamable HTTP at https://outember.io/mcp (your exact URL is in Settings → Integrations). Authenticate with an API key in the Authorization: Bearer oa_live_… header. For example, in Claude Code:
claude mcp add --transport http outember https://outember.io/mcp \
--header "Authorization: Bearer oa_live_…"Tools: the overview, campaigns and their stats, pause and resume, conversations and messages, reply, lead lists, find and add leads, add a list to a campaign, and the do-not-contact list. Tools that act on real people tell the assistant to confirm with you first, and every safety limit still applies. A local (stdio) version of the same tools exists for development; ask us if you need it.
CRM sync
HubSpot and Pipedrive connect in Settings → Integrations with a private app token or an API token; Salesforce with Connect (OAuth). Pick when to sync: when a lead replies, when a reply is interested, or when a connection is accepted. The lead is found by email (or name and company), created if missing (a Salesforce Lead), and the reply is added as a note (a Salesforce Task). No code needed.
Request and response shapes match what the app itself sends and receives. Missing something, or building an integration we should know about? Write to us.