MailFleet API reference
A two-way REST API over your workspace — read campaigns, leads, replies and stats, and create campaigns, add leads and reply to leads from your own tools. JSON in, JSON out, one bearer token, 120 requests a minute.
Authentication
Create a key in Settings → API & webhooks and choose its access: Read & write (read everything, and make changes — create campaigns, add leads, reply to leads) or Read only (for dashboards). Keys created before the API could write are read-only; a change sent with one is answered 403 read_only_key. Keys start with sk_live_ and are shown once — only a hash is stored, so a lost key is revoked and replaced, never recovered. Treat a key as a password: a read & write key can email your leads.
curl -s "https://mailfleet.co/api/v1/stats" \
-H "Authorization: Bearer sk_live_your_key"
An X-API-Key: sk_live_… header works identically if a bearer token is awkward in your client.
API access is a Pro and Scale feature. A key can be created on any plan, but requests from a Trial or Basic workspace are answered with 403. GET /api/v1 needs no key at all and lists what is available.
Conventions
- Base URL —
https://mailfleet.co/api/v1. Every path below is relative to it. - Field names are
snake_case(first_name,sent_today,daily_limit). - Lists accept
limit(1–100, default 25) andoffset(default 0), and answer with{ object: "list", data: [...], pagination: { total, limit, offset, has_more } }. - Scope — a key only ever sees its own workspace. There is no cross-workspace read, for anyone.
- Reads and writes —
GETreads;POST,PUT,PATCHandDELETEchange things and need a read & write key. Every change runs the same code as the app, so it behaves exactly as clicking it would. To be told when something happens rather than polling for it, use webhooks — on every plan, and manageable through the API. - Safe retries — send an
Idempotency-Keyheader (a UUID) with a request that creates or sends something. A retry with the same key and body gets the first response back (withIdempotent-Replayed: true) instead of doing it twice. Keys last 24 hours. - Bodies are JSON (
Content-Type: application/json), up to 5 MB — 2,000 leads with custom fields fit easily. - Machine-readable — the OpenAPI 3.0 document describes everything on this page; point your generator at it.
GET /campaigns
List campaigns
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
status | DRAFT · SENDING · PAUSED · FINISHED | query | Filter by status. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns?status=SENDING" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "SENDING",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-08T08:02:11.000Z",
"created_at": "2026-09-05T16:41:57.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /campaigns
Create a campaign
Create a draft email campaign — with its steps, sender_ids and schedule in the same call if you have them, or set each later. It starts with no steps (never a placeholder email) and sends nothing until you start it with POST /campaigns/{id}/start, which runs the same checks as Launch in the app.
Needs a read & write key. Send an Idempotency-Key so a retried request can't create it twice.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
name | string | required | The campaign's name. |
steps | array of object | optional | The sequence, in order (see PUT /campaigns/{id}/steps). |
sender_ids | array of string | optional | Mailboxes to send from — ids from GET /senders. Empty or left out = every healthy sender. |
schedule | object | optional |
Request
curl -s -X POST "https://mailfleet.co/api/v1/campaigns" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Acme — Q4 outbound","steps":[{"type":"email","subject":"Quick question about {{company}}","body":"Hi {{first_name|there}},\n\nNoticed {{company}} is hiring SDRs — worth a short call?"},{"type":"email","wait_days":3,"subject":"Re: Quick question about {{company}}","body":"Bumping this in case it got buried."}],"sender_ids":["snd_2e10b4"],"schedule":{"timezone":"America/New_York","days":["Mon","Tue","Wed","Thu","Fri"],"window_from":"09:00","window_to":"16:30"}}'
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "DRAFT",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"type": "email",
"step_count": 128,
"sender_ids": [
"…"
],
"schedule": {
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
}
GET /campaigns/{id}
Get a campaign
The campaign's stats, plus its type, schedule, senders (sender_ids; empty = every healthy sender) and how many steps it has.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "DRAFT",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"type": "email",
"step_count": 128,
"sender_ids": [
"…"
],
"schedule": {
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
}
PATCH /campaigns/{id}
Rename a campaign
Steps, senders, schedule, start and pause have their own endpoints.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Body
| Field | Type | Notes | |
|---|---|---|---|
name | string | required |
Request
curl -s -X PATCH "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Acme — Q4 outbound (EU)"}'
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "DRAFT",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"type": "email",
"step_count": 128,
"sender_ids": [
"…"
],
"schedule": {
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
}
DELETE /campaigns/{id}
Delete a campaign
As Delete does in the app: its sequence, enrolments, record of sent emails and tasks go with it. The leads, their lists and the replies they sent stay (no longer linked to it).
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s -X DELETE "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"deleted": true
}
POST /campaigns/{id}/start
Start or resume a campaign
Start a draft or resume a paused campaign — the same checks as Launch in the app: steps with text, leads to send to, and an active sender. A refusal (400 not_ready) lists what's missing in error.blockers. Emails go out within the campaign's schedule. Starting one that's already sending changes nothing.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/start" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "DRAFT",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"type": "email",
"step_count": 128,
"sender_ids": [
"…"
],
"schedule": {
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
}
POST /campaigns/{id}/pause
Pause a campaign
Nothing more goes out until it's started again. Pausing a paused campaign changes nothing; a draft or finished one answers 409 not_sending.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/pause" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "DRAFT",
"stats": {
"leads": 2400,
"sent": 1890,
"opened": 812,
"clicked": 96,
"replied": 74,
"interested": 21,
"bounced": 18,
"skipped": 12,
"delivered": 128,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
},
"started_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"type": "email",
"step_count": 128,
"sender_ids": [
"…"
],
"schedule": {
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
}
GET /campaigns/{id}/steps
Get a campaign's sequence
Every step in order, with the same fields PUT takes — read, edit, write back.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/steps" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "step",
"number": 1,
"type": "email",
"wait_days": 0,
"subject": "Quick question about {{company}}",
"body": "Hi {{first_name|there}},\n\nNoticed {{company}} is hiring SDRs — worth a short call?",
"variants": [
{
"subject": "{{first_name}}, a quick one",
"body": "Hi {{first_name}} — saw {{company}} is growing the SDR team…"
}
],
"due_days": 128,
"note": "…"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
PUT /campaigns/{id}/steps
Replace a campaign's sequence
Replaces every step with these, in order. type is email (sent automatically), manual (an email a person sends from Tasks, prefilled) or call (a call task — needs the calling add-on). Personalise with {{first_name}}, {{company}}, {{title}} … or {{first_name|there}} for a fallback; a lead's custom fields are variables too, so {{email_1_body}} sends each lead their own copy. Blank lines make paragraphs; the mailbox signature is added. Up to 12 steps, 4 A/B variants each; the first step's wait_days is always 0.
Email campaigns only — a Multi Channel or LinkedIn sequence is edited in the app. On a sending campaign this changes what its remaining leads receive, and leads who had finished continue into steps added at the end, as saving in the app does.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
steps | array of object | required | The whole sequence, in order. |
Request
curl -s -X PUT "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/steps" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"steps":[{"type":"email","subject":"{{email_1_subject}}","body":"{{email_1_body}}"},{"type":"email","wait_days":3,"subject":"Re: {{email_1_subject}}","body":"{{email_2_body}}"},{"type":"call","wait_days":2,"subject":"Follow up on the emails","body":"Opener: I sent you a note about …","due_days":2}]}'
Response
{
"object": "list",
"data": [
{
"object": "step",
"number": 1,
"type": "email",
"wait_days": 0,
"subject": "Quick question about {{company}}",
"body": "Hi {{first_name|there}},\n\nNoticed {{company}} is hiring SDRs — worth a short call?",
"variants": [
{
"subject": "{{first_name}}, a quick one",
"body": "Hi {{first_name}} — saw {{company}} is growing the SDR team…"
}
],
"due_days": 128,
"note": "…"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
GET /campaigns/{id}/senders
Get a campaign's senders
The mailboxes it sends from. An empty list = every healthy sender in the workspace.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/senders" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "sender",
"id": "snd_2e10b4",
"email": "sam@outbound.acme.com",
"name": "Sam Reed",
"status": "ACTIVE"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
PUT /campaigns/{id}/senders
Set a campaign's senders
Choose the mailboxes it sends from (ids from GET /senders); an empty list = every healthy sender. Ids that aren't in your workspace are left out and listed in not_found.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Body
| Field | Type | Notes | |
|---|---|---|---|
sender_ids | array of string | required | Mailbox ids from GET /senders. |
Request
curl -s -X PUT "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/senders" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"sender_ids":["snd_2e10b4"]}'
Response
{
"object": "list",
"data": [
{
"object": "sender",
"id": "snd_2e10b4",
"email": "sam@outbound.acme.com",
"name": "Sam Reed",
"status": "ACTIVE"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
GET /campaigns/{id}/schedule
Get a campaign's schedule
When and how fast it sends: timezone, days, the sending window, the gap between emails and new leads a day.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/schedule" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
PATCH /campaigns/{id}/schedule
Change a campaign's schedule
Send only what changes; the rest keeps its value, as does every other setting (tracking, stop rules …), which stays as set in the app. The window is read in the campaign's timezone — or each lead's own, with respect_lead_timezone.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Body
| Field | Type | Notes | |
|---|---|---|---|
timezone | string | optional | |
days | array of Mon · Tue · Wed · Thu · Fri · Sat · Sun | optional | |
window_from | string | optional | |
window_to | string | optional | |
gap_minutes | integer | optional | |
new_leads_per_day | integer | optional | |
respect_lead_timezone | boolean | optional | |
start_date | string | optional | YYYY-MM-DD, or "" to start straight away. |
Request
curl -s -X PATCH "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/schedule" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"timezone":"America/New_York","window_from":"08:30","window_to":"17:00","new_leads_per_day":60}'
Response
{
"object": "schedule",
"timezone": "America/New_York",
"days": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri"
],
"window_from": "09:00",
"window_to": "16:30",
"gap_minutes": 20,
"new_leads_per_day": 100,
"respect_lead_timezone": false,
"start_date": null
}
GET /campaigns/{id}/leads
List a campaign's leads
Who is in the campaign and where each is in the sequence, newest first.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
status | ACTIVE · PAUSED · REPLIED · BOUNCED · UNSUBSCRIBED · FINISHED | query | Filter by enrolment status. |
Request
curl -s "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/leads?status=ACTIVE" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "campaign_lead",
"status": "ACTIVE",
"step": 2,
"waiting_on_task": false,
"last_sent_at": "2026-09-22T09:14:02.000Z",
"next_due_at": "2026-09-22T09:14:02.000Z",
"enrolled_at": "2026-09-22T09:14:02.000Z",
"lead": {
"object": "lead",
"id": "led_4b81e0",
"email": "priya@acme.com",
"first_name": "Priya",
"last_name": "Raman",
"company": "Acme",
"title": "Head of Operations",
"city": "Leeds",
"website": "acme.com",
"phone": "+44 7700 900123",
"linkedin_url": "https://www.linkedin.com/in/example-lead",
"status": "CONTACTED",
"created_at": "2026-09-05T16:42:10.000Z"
}
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /campaigns/{id}/leads
Add leads to a campaign
Send leads (up to 2,000 per call) — they're saved as leads, deduped by email, into list_id or a new list — or just list_id to enrol a whole existing list. Only the people named are enrolled; others in a reused list aren't.
Like an import in the app: blocklisted and unsubscribed people are left out, anyone already in the campaign isn't added twice, new leads are added only while the workspace has room (the leads its plan keeps), and skip_active_elsewhere leaves out anyone still being emailed by another campaign. Enrolled leads start when the campaign is sending, within its schedule. Custom fields become {{variables}} for the copy.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
leads | array of object | optional | The people to add (or send list_id alone). |
list_id | string | optional | Save them into this existing list — or, without leads, enrol this whole list. |
list_name | string | optional | Name for the new list the leads go into (when there's no list_id). |
skip_active_elsewhere | boolean | optional | Leave out people still being emailed by another campaign. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/leads" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"leads":[{"email":"priya@acme.com","first_name":"Priya","company":"Acme","custom":{"email_1_subject":"Priya — the Leeds depot","email_1_body":"Congrats on opening the Leeds depot…"}},{"email":"tom@northwind.io","first_name":"Tom","company":"Northwind"}],"list_name":"Q4 — logistics"}'
Response
{
"object": "enrollment",
"campaign_id": "cmp_9f2a1c",
"list_id": "lst_9a12c4",
"imported": 180,
"updated": 14,
"skipped": 4,
"blocked": 2,
"enrolled": 192,
"in_list": null,
"skipped_active_elsewhere": 0,
"campaign_status": "SENDING",
"errors": [
{
"row": 7,
"email": "not-an-address",
"reason": "That isn't a valid email address."
}
]
}
DELETE /campaigns/{id}/leads/{lead_id}
Remove a lead from a campaign
Nothing more is sent to them from this campaign; the lead, their lists and what was already sent stay. An email campaign's enrolment is deleted (adding them again starts at step 1); a Multi Channel or LinkedIn campaign's is ended instead (ended: true), so the campaign can't add them back on its own.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The campaign's id. |
lead_id | string | required | The lead's id. |
Request
curl -s -X DELETE "https://mailfleet.co/api/v1/campaigns/cmp_9f2a1c/leads/led_4b81e0" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign_lead",
"campaign_id": "cmp_9f2a1c",
"lead_id": "led_4b81e0",
"removed": true,
"ended": false,
"campaign_leads": 2399
}
GET /leads
List leads
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
status | NEW · CONTACTED · REPLIED · MEETING · WON · BOUNCED · UNSUBSCRIBED · BLOCKED | query | Filter by status. |
search | string | query | Case-insensitive match on email, company or name. |
email | string | query | One exact address — find a lead to update. |
list_id | string | query | Only the leads in this list. |
Request
curl -s "https://mailfleet.co/api/v1/leads?status=CONTACTED" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "lead",
"id": "led_4b81e0",
"email": "priya@acme.com",
"first_name": "Priya",
"last_name": "Raman",
"company": "Acme",
"title": "Head of Operations",
"city": "Leeds",
"website": "acme.com",
"phone": "+44 7700 900123",
"linkedin_url": "https://www.linkedin.com/in/example-lead",
"status": "CONTACTED",
"created_at": "2026-09-05T16:42:10.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /leads
Add leads
Add or update up to 2,000 leads without enrolling them anywhere — the same import as a CSV upload in the app: deduped by email across the workspace (an existing lead is updated and moved into the list), blocklisted addresses refused, and new leads added only up to the room the workspace has left (the leads its plan keeps; a full workspace adds none and says so in errors). Custom fields become {{variables}} for campaign copy. To enrol them as well, use POST /campaigns/{id}/leads.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
leads | array of object | required | |
list_id | string | optional | Into this existing list. |
list_name | string | optional | Else a new list with this name (default "Added via API"). |
Request
curl -s -X POST "https://mailfleet.co/api/v1/leads" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"leads":[{"email":"priya@acme.com","first_name":"Priya","last_name":"Raman","company":"Acme","title":"Head of Operations"}],"list_id":"lst_9a12c4"}'
Response
{
"object": "import",
"list_id": "lst_9a12c4",
"list_name": "Added via API",
"imported": 180,
"updated": 14,
"skipped": 4,
"blocked": 2,
"custom_fields": [
"email_1_body"
],
"errors": [
{
"row": 7,
"email": "not-an-address",
"reason": "That isn't a valid email address."
}
]
}
GET /leads/{id}
Get a lead
The lead with its custom fields ({{variables}}), its list, and the campaigns it's in — status and step in each.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The lead's id. |
Request
curl -s "https://mailfleet.co/api/v1/leads/led_4b81e0" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "lead",
"id": "cmp_9f2a1c",
"email": "priya@acme.com",
"first_name": "Priya",
"last_name": "Raman",
"company": "Acme",
"title": "Head of Operations",
"city": "Leeds",
"website": "acme.com",
"phone": "…",
"linkedin_url": "…",
"status": "NEW",
"created_at": "2026-09-22T09:14:02.000Z",
"list_id": "lst_9a12c4",
"timezone": "Europe/London",
"custom": {
"email_1_body": "Priya — congrats on the Leeds depot…"
},
"campaigns": [
{
"campaign_id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"campaign_status": "DRAFT",
"status": "ACTIVE",
"step": 128,
"last_sent_at": "2026-09-22T09:14:02.000Z",
"next_due_at": "2026-09-22T09:14:02.000Z"
}
]
}
PATCH /leads/{id}
Update a lead
Any field — an empty string clears it — and custom fields: a key set to "" is removed, others are added or replaced, the rest kept. The same edit as the lead panel in the app. An address another lead already uses is refused (409 email_taken).
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The lead's id. |
Body
| Field | Type | Notes | |
|---|---|---|---|
email | string | optional | |
first_name | string | optional | |
last_name | string | optional | |
company | string | optional | |
title | string | optional | |
website | string | optional | |
industry | string | optional | |
employee_count | string | optional | |
city | string | optional | |
country | string | optional | |
phone | string | optional | |
linkedin_url | string | optional | |
timezone | string | optional | IANA timezone (used when a campaign respects each lead's timezone). |
custom | object | optional | Keys of letters, digits and underscores. "" removes a key; others are added or replaced. |
Request
curl -s -X PATCH "https://mailfleet.co/api/v1/leads/led_4b81e0" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"company":"Acme Logistics","custom":{"email_1_body":"Priya — congrats on the Leeds depot…"}}'
Response
{
"object": "lead",
"id": "cmp_9f2a1c",
"email": "priya@acme.com",
"first_name": "Priya",
"last_name": "Raman",
"company": "Acme",
"title": "Head of Operations",
"city": "Leeds",
"website": "acme.com",
"phone": "…",
"linkedin_url": "…",
"status": "NEW",
"created_at": "2026-09-22T09:14:02.000Z",
"list_id": "lst_9a12c4",
"timezone": "Europe/London",
"custom": {
"email_1_body": "Priya — congrats on the Leeds depot…"
},
"campaigns": [
{
"campaign_id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"campaign_status": "DRAFT",
"status": "ACTIVE",
"step": 128,
"last_sent_at": "2026-09-22T09:14:02.000Z",
"next_due_at": "2026-09-22T09:14:02.000Z"
}
]
}
POST /leads/{id}/unsubscribe
Unsubscribe a lead
As if they clicked the unsubscribe link: marked Unsubscribed, stopped in every campaign still sending or paused, never enrolled again, and the lead.unsubscribed webhook fires. To block an address or a whole domain, use the blocklist.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The lead's id. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/leads/led_4b81e0/unsubscribe" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "lead",
"id": "led_4b81e0",
"email": "priya@acme.com",
"status": "UNSUBSCRIBED",
"campaigns_stopped": 1
}
GET /lists
List lead lists
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
Request
curl -s "https://mailfleet.co/api/v1/lists" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "list",
"id": "lst_9a12c4",
"name": "Med spas — Texas",
"leads": 412,
"created_at": "2026-09-22T09:14:02.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /lists
Create a list
An empty list; add leads to it with POST /leads and its list_id.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
name | string | required |
Request
curl -s -X POST "https://mailfleet.co/api/v1/lists" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Med spas — Texas"}'
Response
{
"object": "list",
"id": "lst_9a12c4",
"name": "Med spas — Texas",
"leads": 412,
"created_at": "2026-09-22T09:14:02.000Z"
}
GET /replies
List replies
Newest first. To sync your inbox, poll with received_after (the newest received_at you've seen) — or subscribe to the reply.received webhook instead.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
intent | INTERESTED · MEETING · QUESTION · NOT_NOW · NEUTRAL · UNSUBSCRIBE | query | Filter by detected intent. |
campaign_id | string | query | Filter by campaign. |
lead_id | string | query | One lead's replies. |
handled | boolean | query | true = handled only, false = still open. |
received_after | string (date-time) | query | Only replies received after this time (ISO 8601). |
Request
curl -s "https://mailfleet.co/api/v1/replies?intent=INTERESTED" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "reply",
"id": "rep_77c3a9",
"from_email": "priya@acme.com",
"from_name": "Priya Raman",
"subject": "Re: Quick question about Acme",
"preview": "Happy to take a look — how does Thursday suit?",
"intent": "INTERESTED",
"campaign_id": "cmp_9f2a1c",
"lead_id": "led_4b81e0",
"sender_id": "snd_2e10b4",
"handled": false,
"received_at": "2026-09-22T09:14:02.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
GET /replies/{id}
Get a conversation
The whole thread: every campaign email the lead was sent (step, delivery, when they opened or clicked), their replies and yours, in order — plus how it's classified, whether it's handled, and teammates' notes. Each message's text is the reply without the quoted history.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The reply's id. |
Request
curl -s "https://mailfleet.co/api/v1/replies/rep_77c3a9" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "conversation",
"id": "rep_77c3a9",
"subject": "Re: Quick question about Acme",
"class": "Interested",
"intent": "INTERESTED",
"tags": [
"hot"
],
"handled": false,
"read": true,
"starred": false,
"snoozed_until": null,
"received_at": "2026-09-22T09:14:02.000Z",
"sender": "sam@outbound.acme.com",
"lead": {
"id": "cmp_9f2a1c",
"email": "priya@acme.com",
"name": "Priya Raman",
"company": "Acme",
"title": "Head of Operations",
"phone": "…",
"status": "REPLIED"
},
"campaign": {
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "…"
},
"messages": [
{
"direction": "in",
"kind": "reply_received",
"from": "priya@acme.com",
"to": "sam@outbound.acme.com",
"subject": "Re: Quick question about Acme",
"text": "Happy to take a look — how does Thursday suit?",
"text_truncated": true,
"at": "2026-09-22T09:14:02.000Z",
"step": 128,
"campaign": "…",
"delivery": "…",
"opened_at": "2026-09-22T09:14:02.000Z",
"clicked_at": "2026-09-22T09:14:02.000Z"
}
],
"notes": [
{
"kind": "note",
"by": "…",
"at": "2026-09-22T09:14:02.000Z",
"body": "…",
"due_at": "2026-09-22T09:14:02.000Z",
"done": true
}
],
"not_shown": "…"
}
PATCH /replies/{id}
Mark a conversation
As the Replies inbox does. mark is "Mark lead as": Interested, Meeting request, Question, Not now, Not interested, OOO, Wrong person or Referral classify the thread (and start any subsequence waiting on that tag); Meeting booked and Won also move the lead's status (Won marks it handled); Unsubscribed stops the lead in every campaign. handled and read cover the whole conversation; tags are your own labels (the classification is set with mark).
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The reply's id. |
Body
| Field | Type | Notes | |
|---|---|---|---|
mark | Interested · Meeting request · Question · Not now · Not interested · OOO · Wrong person · Referral · Meeting booked · Won · Unsubscribed | optional | "Mark lead as" — the classification and what goes with it. |
handled | boolean | optional | Resolve (or reopen) the whole conversation. |
read | boolean | optional | |
tags | array of string | optional | Your own labels (replaces them). |
starred | boolean | optional | |
snoozed_until | string (date-time), nullable | optional | Out of the inbox until then (within a year); null wakes it. |
Request
curl -s -X PATCH "https://mailfleet.co/api/v1/replies/rep_77c3a9" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"mark":"Meeting booked","handled":true}'
Response
{
"object": "conversation",
"id": "rep_77c3a9",
"subject": "Re: Quick question about Acme",
"class": "Interested",
"intent": "INTERESTED",
"tags": [
"hot"
],
"handled": false,
"read": true,
"starred": false,
"snoozed_until": null,
"received_at": "2026-09-22T09:14:02.000Z",
"sender": "sam@outbound.acme.com",
"lead": {
"id": "cmp_9f2a1c",
"email": "priya@acme.com",
"name": "Priya Raman",
"company": "Acme",
"title": "Head of Operations",
"phone": "…",
"status": "REPLIED"
},
"campaign": {
"id": "cmp_9f2a1c",
"name": "Acme — Q4 outbound",
"status": "…"
},
"messages": [
{
"direction": "in",
"kind": "reply_received",
"from": "priya@acme.com",
"to": "sam@outbound.acme.com",
"subject": "Re: Quick question about Acme",
"text": "Happy to take a look — how does Thursday suit?",
"text_truncated": true,
"at": "2026-09-22T09:14:02.000Z",
"step": 128,
"campaign": "…",
"delivery": "…",
"opened_at": "2026-09-22T09:14:02.000Z",
"clicked_at": "2026-09-22T09:14:02.000Z"
}
],
"notes": [
{
"kind": "note",
"by": "…",
"at": "2026-09-22T09:14:02.000Z",
"body": "…",
"due_at": "2026-09-22T09:14:02.000Z",
"done": true
}
],
"not_shown": "…"
}
POST /replies/{id}/reply
Reply to a lead
A real email, sent from the mailbox their message arrived on, in the same thread ("Re:" the subject) — exactly as replying in the Replies inbox: plain text (blank lines make paragraphs), the mailbox's CRM copy if it has one, a slot of its daily limit, and it goes out even while that mailbox is paused.
The same text can't go to the same conversation twice within a minute (409 duplicate_reply); send an Idempotency-Key to retry safely. It doesn't mark the conversation handled — PATCH it for that.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The reply's id. |
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
body | string | required | The reply's text. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/replies/rep_77c3a9/reply" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"body":"Thanks Priya — Thursday at 2pm works. I'll send an invite."}'
Response
{
"object": "message",
"direction": "out",
"reply_id": "rep_77c3a9",
"to": "priya@acme.com",
"subject": "Re: Quick question about Acme",
"sender_id": "snd_2e10b4",
"sent_at": "2026-09-22T09:14:02.000Z"
}
GET /senders
List senders (mailboxes)
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
status | ACTIVE · UNVERIFIED · ERROR · PAUSED | query | Filter by status. |
Request
curl -s "https://mailfleet.co/api/v1/senders?status=ACTIVE" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "sender",
"id": "snd_2e10b4",
"email": "sam@outbound.acme.com",
"name": "Sam Reed",
"domain": "outbound.acme.com",
"provider": "google",
"status": "ACTIVE",
"health": 92,
"daily_limit": 40,
"sent_today": 22,
"gap_minutes": 20,
"warmup_enabled": true,
"has_imap": true,
"last_error": null,
"created_at": "2026-07-19T11:23:04.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
GET /stats
Workspace summary stats
Parameters
No parameters.
Request
curl -s "https://mailfleet.co/api/v1/stats" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "stats",
"campaigns": {
"total": 9,
"sending": 3
},
"senders": {
"total": 14,
"active": 11
},
"leads": {
"total": 2400
},
"replies": {
"total": 74,
"unhandled": 9
},
"emails": {
"sent_all_time": 41908,
"sent_today": 388,
"delivered_all_time": 128
},
"engagement": {
"replied": 74,
"interested": 21,
"bounced": 18,
"hard_bounced": 128,
"blocked": 128,
"leads_reached": 128,
"auto_replied": 128
}
}
GET /blocklist
List the blocklist
Addresses and whole domains that are never contacted, newest first.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
kind | email · domain | query | Only addresses or only domains. |
Request
curl -s "https://mailfleet.co/api/v1/blocklist?kind=domain" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "blocklist_entry",
"id": "cmp_9f2a1c",
"value": "competitor.com",
"kind": "domain",
"note": "api",
"created_at": "2026-09-22T09:14:02.000Z"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /blocklist
Block addresses or domains
As Settings → Blocklist does: leads already in the workspace that match are marked Blocked and stopped in every campaign, and a blocked address is never imported or enrolled again. A URL counts as its domain. Up to 20,000 entries per call.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
entries | array of string | required | |
note | string | optional | Where they came from (shown in Settings). |
Request
curl -s -X POST "https://mailfleet.co/api/v1/blocklist" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"entries":["jo@competitor.com","competitor.com"]}'
Response
{
"object": "blocklist_result",
"added": 2,
"already_blocked": 0,
"invalid": [],
"leads_blocked": 1
}
GET /webhooks
List webhooks
Your endpoints and the events each gets. Signing secrets aren't listed — each is shown once, when it's created.
Parameters
No parameters.
Request
curl -s "https://mailfleet.co/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "webhook",
"id": "whk_1d4f07",
"url": "https://your-app.com/hooks/mailfleet",
"events": [
"reply.received",
"reply.interested"
],
"active": true,
"last_status": 200,
"last_fired_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"secret": "whsec_…"
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true
}
}
POST /webhooks
Create a webhook
A signed POST to your URL the moment each chosen event happens (X-MailFleet-Signature: HMAC-SHA256 of the raw body with the secret), retried for 24 hours if your endpoint is down. The secret is in this response only — keep it. Up to 25 webhooks per workspace.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
Idempotency-Key | string | header | Optional: a unique key (a UUID) so a retry of this request replays the first response instead of doing it twice. Kept for 24 hours. |
Body
| Field | Type | Notes | |
|---|---|---|---|
url | string (uri) | required | |
events | array of email.sent · email.opened · link.clicked · reply.received · reply.interested · reply.tagged · email.bounced · lead.unsubscribed · lead.enriched · deal.created · deal.stage_changed · deal.won · deal.lost | required |
Request
curl -s -X POST "https://mailfleet.co/api/v1/webhooks" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-app.com/hooks/mailfleet","events":["reply.received","reply.interested"]}'
Response
{
"object": "webhook",
"id": "whk_1d4f07",
"url": "https://your-app.com/hooks/mailfleet",
"events": [
"reply.received",
"reply.interested"
],
"active": true,
"last_status": 200,
"last_fired_at": "2026-09-22T09:14:02.000Z",
"created_at": "2026-09-22T09:14:02.000Z",
"secret": "whsec_…"
}
DELETE /webhooks/{id}
Delete a webhook
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required | The webhook's id. |
Request
curl -s -X DELETE "https://mailfleet.co/api/v1/webhooks/whk_1d4f07" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "campaign",
"id": "cmp_9f2a1c",
"deleted": true
}
GET /leaddb/search
Search the Lead Database
Search 53M people by title, seniority, location, industry and company size. Free — only a reveal costs anything.
Addresses come back masked. /leaddb/reveal is the only path to a full address. Array filters repeat the key: ?seniority=vp&seniority=head.
pagination.capped is true when more than 10,000 people match; total is then a floor, not a count. Rate limit: 60 requests/minute.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
limit | integer | query | Page size, 1–100 (default 25). Default 25. |
offset | integer | query | Number of items to skip (default 0). Default 0. |
q | string | query | Free text across name, title and company. |
title | string | query | Single job-title match. |
titles | array of string | query | Any-of job titles (repeat the key, or comma-separate). |
exclude_titles | array of string | query | None-of job titles. |
similar_titles | boolean | query | Expand each title to its common variants. |
seniority | array of c_suite · founder · owner · partner · vp · head · director · manager · senior · entry · intern | query | Any-of seniority bands. |
countries | array of string | query | Any-of countries. |
states | array of string | query | Any-of states or regions. |
industries | array of string | query | Any-of industries. |
employees | array of string | query | Any-of company-size bands. |
email | any · has · verified | query | Require an address, or a verified one. |
phone | boolean | query | Only people with a phone number on file. |
saved | all · saved · new | query | All people, only ones you have already acquired, or only new ones. |
Request
curl -s "https://mailfleet.co/api/v1/leaddb/search?q=Priya%20Raman" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "list",
"data": [
{
"object": "leaddb_person",
"id": "48210337",
"full_name": "Priya Raman",
"first_name": "Priya",
"last_name": "Raman",
"title": "Head of Operations",
"seniority": "head",
"email_masked": "p****@acme.com",
"email_status": "verified",
"email_corporate": true,
"email_check": "valid",
"email_checked_at": "2026-09-19T04:11:20.000Z",
"has_phone": true,
"linkedin_url": "https://www.linkedin.com/in/example-lead",
"city": "Leeds",
"state": "West Yorkshire",
"country": "United Kingdom",
"company": {
"name": "Acme",
"domain": "acme.com",
"industry": "Logistics",
"employees": 120
},
"acquired": false
}
],
"pagination": {
"total": 214,
"limit": 25,
"offset": 0,
"has_more": true,
"capped": false,
"count_cap": 10000
}
}
POST /leaddb/reveal
Reveal a person's email address
The only endpoint that returns an unmasked address, and the only one that spends anything.
You pay once per person, ever. Revealing someone you have already acquired returns charged: false and costs nothing. A charged reveal spends one unit of the monthly Lead Database allowance (and one lead credit on a workspace still on monthly lead credits). Nothing is charged unless an address is handed over.
A reveal saves the person to your leads. The lead takes one place in your workspace like any other, in a list named "Revealed from Lead Database" (or the add_to_list list), with the same fields an add brings; deleting it frees the place, and revealing them again later is free of the allowance and saves them again. So a reveal needs one free place, unless the person is already one of your leads (matched by email): with none, it answers 402 lead_storage_full and charges nothing. saved_to_leads, lead_list and lead_id say where they are. If the address was handed over but the lead couldn't be written (or the address would bounce, or is blocklisted), saved_to_leads is false and save_note says why. A workspace still on monthly lead credits keeps nothing from a reveal, as before — use add_to_list.
The address is verified live at reveal time (a verdict under 24 hours old is reused). One person per call. Rate limit: 60 requests/minute.
Parameters
No parameters.
Body
| Field | Type | Notes | |
|---|---|---|---|
person_id | string | required | The id of a person from /leaddb/search. |
add_to_list | string | optional | The list the person is saved into as a lead — the id of one of your lead lists, or new for a fresh one — instead of "Revealed from Lead Database". On a workspace still on monthly lead credits, which keeps nothing from a reveal, this is what adds them. Free: the import skips anyone already acquired. |
Request
curl -s -X POST "https://mailfleet.co/api/v1/leaddb/reveal" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"person_id":"48210337"}'
Response
{
"object": "leaddb_reveal",
"person_id": "48210337",
"email": "priya.raman@acme.com",
"verification": {
"outcome": "valid",
"code": null,
"checked_at": "2026-09-22T09:14:02.000Z",
"cached": false
},
"charged": true,
"lead_id": "led_4b81e0",
"saved_to_leads": true,
"lead_list": {
"id": "cmp_9f2a1c",
"name": "Revealed from Lead Database"
},
"save_note": "…",
"quota": {
"used": 1841,
"cap": 5000,
"remaining": 3159,
"period": "2026-09"
}
}
POST /leads/{id}/enrich
Find a lead's mobile number
Reveal the lead's own mobile or direct line through your Apollo key (Settings → Integrations). The company switchboard is never substituted.
Asynchronous. Apollo sends the number back a little later, so this answers 202 pending. Subscribe to the lead.enriched webhook rather than polling — it fires the moment the reveal settles, found or not.
Spends no MailFleet credit; Apollo bills your own key. Limits: 30/minute and 500/day per workspace, so a runaway script cannot drain your Apollo balance.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required |
Request
curl -s -X POST "https://mailfleet.co/api/v1/leads/led_4b81e0/enrich" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "enrichment",
"id": "enr_71c0aa",
"lead_id": "led_4b81e0",
"status": "pending"
}
GET /leads/{id}/enrich
Check an enrichment
The lead's current number and the state of its most recent enrichment. status is null when the lead has never been enriched.
Parameters
| Parameter | Type | In | Notes |
|---|---|---|---|
id | string | required |
Request
curl -s "https://mailfleet.co/api/v1/leads/led_4b81e0/enrich" \
-H "Authorization: Bearer sk_live_your_key"
Response
{
"object": "enrichment",
"id": "enr_71c0aa",
"lead_id": "led_4b81e0",
"status": "done",
"phone": "+44 7700 900123",
"updated_at": "2026-09-22T09:16:41.000Z"
}
Objects
Campaign
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
name | string | |
status | DRAFT · SENDING · PAUSED · FINISHED | |
stats | objectleads sent opened clicked replied interested bounced skipped delivered hard_bounced blocked leads_reached auto_replied | Counted from the campaign's emails and replies. sent, delivered, bounced, hard_bounced and blocked count emails; opened, clicked, replied and interested count people, out of leads_reached. Every recorded open counts, including the automatic ones mail-security scanners make on delivery; clicks in the first 5 minutes after sending are left out (scanners follow every link on delivery), and so are out-of-office and auto-replies. |
started_at | string (date-time), nullable | |
created_at | string (date-time) |
Sender
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
email | string | |
name | string | |
domain | string | |
provider | string | |
status | ACTIVE · UNVERIFIED · ERROR · PAUSED | |
health | integer | 0–100 reputation score. |
daily_limit | integer | |
sent_today | integer | |
gap_minutes | integer | |
warmup_enabled | boolean | |
has_imap | boolean | |
last_error | string, nullable | |
created_at | string (date-time) |
Lead
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
email | string | |
first_name | string, nullable | |
last_name | string, nullable | |
company | string, nullable | |
title | string, nullable | |
city | string, nullable | |
website | string, nullable | |
phone | string, nullable | Filled in by /leads/{id}/enrich, or from your import. |
linkedin_url | string, nullable | |
status | NEW · CONTACTED · REPLIED · MEETING · WON · BOUNCED · UNSUBSCRIBED · BLOCKED | |
created_at | string (date-time) |
Reply
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
from_email | string | |
from_name | string, nullable | |
subject | string, nullable | |
preview | string, nullable | |
intent | INTERESTED · MEETING · QUESTION · NOT_NOW · NEUTRAL · UNSUBSCRIBE | |
campaign_id | string, nullable | |
lead_id | string, nullable | |
sender_id | string, nullable | |
handled | boolean | |
received_at | string (date-time) |
CampaignDetail
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
name | string | |
status | DRAFT · SENDING · PAUSED · FINISHED | |
stats | objectleads sent opened clicked replied interested bounced skipped delivered hard_bounced blocked leads_reached auto_replied | Counted from the campaign's emails and replies. sent, delivered, bounced, hard_bounced and blocked count emails; opened, clicked, replied and interested count people, out of leads_reached. Every recorded open counts, including the automatic ones mail-security scanners make on delivery; clicks in the first 5 minutes after sending are left out (scanners follow every link on delivery), and so are out-of-office and auto-replies. |
started_at | string (date-time), nullable | |
created_at | string (date-time) | |
type | email · multichannel · linkedin · subsequence | email campaigns are the ones the API writes sequences for. |
step_count | integer | Steps in the sequence. |
sender_ids | array of string | The mailboxes it sends from. Empty = every healthy sender. |
schedule | objectobject timezone days window_from window_to gap_minutes new_leads_per_day respect_lead_timezone start_date |
Schedule
| Field | Type | Notes |
|---|---|---|
object | string | |
timezone | string | IANA timezone the window is read in. |
days | array of Mon · Tue · Wed · Thu · Fri · Sat · Sun | Days it sends on. |
window_from | string | Start of the sending window, 24-hour HH:MM. |
window_to | string | End of the sending window. |
gap_minutes | integer | Minutes between two emails from the same mailbox (1–240). |
new_leads_per_day | integer | First emails a day, across the campaign. |
respect_lead_timezone | boolean | Read the window in each lead's own timezone (their timezone field), else the campaign's. |
start_date | string, nullable | YYYY-MM-DD; null = start straight away. |
ScheduleInput
| Field | Type | Notes |
|---|---|---|
timezone | string | |
days | array of Mon · Tue · Wed · Thu · Fri · Sat · Sun | |
window_from | string | |
window_to | string | |
gap_minutes | integer | |
new_leads_per_day | integer | |
respect_lead_timezone | boolean | |
start_date | string | YYYY-MM-DD, or "" to start straight away. |
Step
| Field | Type | Notes |
|---|---|---|
object | string | |
number | integer | Position in the sequence, from 1. |
type | email · manual · call | Multi Channel sequences also have linkedin_* and condition steps. |
wait_days | integer | Days after the previous step (0 for the first). |
subject | string | Email subject, or the call objective. |
body | string | Email body or call script. |
variants | array of object | A/B variants (up to 4). |
due_days | integer, nullable | manual/call: days the person has to do the task. |
note | string | manual/call: an internal note shown with the task. |
StepInput
| Field | Type | Notes |
|---|---|---|
type | email · manual · call | Default email. call needs the calling add-on. |
subject | string | Email subject, or the call objective. |
body | string | Required for email and manual steps. |
wait_days | integer | Days after the previous step (default 3; always 0 for the first). |
due_days | integer | manual/call: days to do the task (default 2). |
note | string | manual/call: an internal note. |
variants | array of object |
Variant
| Field | Type | Notes |
|---|---|---|
subject | string | |
body | string |
CampaignSender
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
email | string | |
name | string | |
status | ACTIVE · UNVERIFIED · ERROR · PAUSED |
CampaignLead
| Field | Type | Notes |
|---|---|---|
object | string | |
status | ACTIVE · PAUSED · REPLIED · BOUNCED · UNSUBSCRIBED · FINISHED | |
step | integer | The step they're on, from 1 (past the last step once finished). |
waiting_on_task | boolean | Parked on a manual or call task until someone completes it. |
last_sent_at | string (date-time), nullable | |
next_due_at | string (date-time), nullable | When their next step is due. |
enrolled_at | string (date-time) | |
lead | objectobject id email first_name last_name company title city website phone linkedin_url status created_at |
CampaignLeadRemoved
| Field | Type | Notes |
|---|---|---|
object | string | |
campaign_id | string | |
lead_id | string | |
removed | boolean | |
ended | boolean | A Multi Channel or LinkedIn enrolment is ended rather than deleted. |
campaign_leads | integer | People left in the campaign. |
LeadInput
| Field | Type | Notes |
|---|---|---|
email | string | |
first_name | string | |
last_name | string | |
company | string | |
title | string | |
city | string | |
country | string | |
website | string | |
industry | string | |
phone | string | |
linkedin_url | string | |
custom | object | Extra fields, each a {{variable}} in the copy ("Email 1 Body" becomes email_1_body). |
LeadEdit
| Field | Type | Notes |
|---|---|---|
email | string | |
first_name | string | |
last_name | string | |
company | string | |
title | string | |
website | string | |
industry | string | |
employee_count | string | |
city | string | |
country | string | |
phone | string | |
linkedin_url | string | |
timezone | string | IANA timezone (used when a campaign respects each lead's timezone). |
custom | object | Keys of letters, digits and underscores. "" removes a key; others are added or replaced. |
LeadDetail
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
email | string | |
first_name | string, nullable | |
last_name | string, nullable | |
company | string, nullable | |
title | string, nullable | |
city | string, nullable | |
website | string, nullable | |
phone | string, nullable | Filled in by /leads/{id}/enrich, or from your import. |
linkedin_url | string, nullable | |
status | NEW · CONTACTED · REPLIED · MEETING · WON · BOUNCED · UNSUBSCRIBED · BLOCKED | |
created_at | string (date-time) | |
list_id | string, nullable | |
timezone | string, nullable | |
custom | object | The lead's custom fields. |
campaigns | array of object | The campaigns it's in (the 50 most recent). |
LeadUnsubscribed
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
email | string | |
status | string | |
campaigns_stopped | integer |
Import
| Field | Type | Notes |
|---|---|---|
object | string | |
list_id | string | |
list_name | string | |
imported | integer | New leads. |
updated | integer | Leads already in the workspace, updated and moved into the list. |
skipped | integer | Rows without a usable address, or past the room the workspace has for new leads. |
blocked | integer | On the blocklist — not added. |
custom_fields | array of string | |
errors | array of object |
Enrollment
| Field | Type | Notes |
|---|---|---|
object | string | |
campaign_id | string | |
list_id | string | |
imported | integer | |
updated | integer | |
skipped | integer | |
blocked | integer | |
enrolled | integer | People added to the campaign now. |
in_list | integer, nullable | When enrolling a whole list: its size. |
skipped_active_elsewhere | integer | |
campaign_status | DRAFT · SENDING · PAUSED · FINISHED | |
errors | array of object |
RowError
| Field | Type | Notes |
|---|---|---|
row | integer | |
email | string | |
reason | string |
List
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
name | string | |
leads | integer | |
created_at | string (date-time) |
Conversation
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
subject | string, nullable | |
class | string | How the inbox classifies it: a mark (Interested, Meeting request …), or Reply. |
intent | INTERESTED · MEETING · QUESTION · NOT_NOW · NEUTRAL · UNSUBSCRIBE | |
tags | array of string | Your own labels. |
handled | boolean | |
read | boolean | |
starred | boolean | |
snoozed_until | string (date-time), nullable | |
received_at | string (date-time) | |
sender | string, nullable | The mailbox it arrived on. |
lead | objectid email name company title phone status | |
campaign | object, nullableid name status | |
messages | array of object | |
notes | array of object | |
not_shown | string | Present when a very long history was cut: what was left out. |
Message
| Field | Type | Notes |
|---|---|---|
direction | out · in | |
kind | campaign_email · reply_received · reply_sent | |
from | string, nullable | |
to | string, nullable | |
subject | string, nullable | |
text | string | Without the quoted history; up to 4,000 characters. |
text_truncated | boolean | Present (true) when the text was cut. |
at | string (date-time) | |
step | integer, nullable | campaign_email: which step it was. |
campaign | string, nullable | campaign_email: the campaign's name. |
delivery | string, nullable | campaign_email: delivered, bounced or blocked. |
opened_at | string (date-time), nullable | campaign_email: first recorded open. |
clicked_at | string (date-time), nullable | campaign_email: first click by a person. |
ReplyPatch
| Field | Type | Notes |
|---|---|---|
mark | Interested · Meeting request · Question · Not now · Not interested · OOO · Wrong person · Referral · Meeting booked · Won · Unsubscribed | "Mark lead as" — the classification and what goes with it. |
handled | boolean | Resolve (or reopen) the whole conversation. |
read | boolean | |
tags | array of string | Your own labels (replaces them). |
starred | boolean | |
snoozed_until | string (date-time), nullable | Out of the inbox until then (within a year); null wakes it. |
SentMessage
| Field | Type | Notes |
|---|---|---|
object | string | |
direction | string | |
reply_id | string | |
to | string | |
subject | string | |
sender_id | string | |
sent_at | string (date-time) |
BlocklistEntry
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
value | string | |
kind | email · domain | |
note | string, nullable | |
created_at | string (date-time) |
BlocklistResult
| Field | Type | Notes |
|---|---|---|
object | string | |
added | integer | |
already_blocked | integer | |
invalid | array of string | Entries that aren't an address or a domain. |
leads_blocked | integer | Existing leads now Blocked and stopped. |
Webhook
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
url | string | |
events | array of email.sent · email.opened · link.clicked · reply.received · reply.interested · reply.tagged · email.bounced · lead.unsubscribed · lead.enriched · deal.created · deal.stage_changed · deal.won · deal.lost | |
active | boolean | |
last_status | integer, nullable | |
last_fired_at | string (date-time), nullable | |
created_at | string (date-time) | |
secret | string | Only when it's created — keep it to verify signatures. |
Deleted
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
deleted | boolean |
LeadDbPerson
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | Lead Database person id — the handle for /leaddb/reveal. |
full_name | string | |
first_name | string, nullable | |
last_name | string, nullable | |
title | string, nullable | |
seniority | string, nullable | |
email_masked | string, nullable | Masked. Reveal the address with /leaddb/reveal. |
email_status | string, nullable | What the source data claims: verified, extrapolated or unavailable. |
email_corporate | boolean | |
email_check | string, nullable | Our verifier's own last verdict — stronger than email_status. Null = never probed. |
email_checked_at | string (date-time), nullable | |
has_phone | boolean | |
linkedin_url | string, nullable | |
city | string, nullable | |
state | string, nullable | |
country | string, nullable | |
company | objectname domain industry employees | |
acquired | boolean | You have already paid for this person — revealing them again is free. |
LeadDbPagination
| Field | Type | Notes |
|---|---|---|
total | integer | |
limit | integer | |
offset | integer | |
has_more | boolean | |
capped | boolean | More than count_cap people match; total is a floor, not a count. |
count_cap | integer |
LeadDbReveal
| Field | Type | Notes |
|---|---|---|
object | string | |
person_id | string | |
email | string | |
verification | objectoutcome code checked_at cached | |
charged | boolean | False when you had already paid for this person. |
lead_id | string, nullable | The lead this person is in your workspace — saved by the reveal, already there, or added by add_to_list. Null when they aren't one of your leads. |
saved_to_leads | boolean | The person is one of your leads after this call: saved by the reveal (it takes one place in your workspace), or already one. False on a workspace still on monthly lead credits unless add_to_list was used, and when the save didn't happen — see save_note. |
lead_list | object, nullableid name | The list the lead is in — "Revealed from Lead Database" unless add_to_list named another. Null when not a lead, or in no list. |
save_note | string, nullable | Why the address was handed over but the person wasn't saved to your leads (the address would bounce, it's blocklisted, the workspace filled up meanwhile, or a fault of ours), in a plain sentence. Null otherwise. |
quota | objectused cap remaining period | Your Lead Database allowance after this call. Null caps mean unlimited. |
EnrichmentPending
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string | |
lead_id | string | |
status | pending |
Enrichment
| Field | Type | Notes |
|---|---|---|
object | string | |
id | string, nullable | |
lead_id | string | |
status | pending · done · not_found · failed | |
phone | string, nullable | The person's mobile or direct line. Never the company switchboard. |
updated_at | string (date-time), nullable |
Stats
| Field | Type | Notes |
|---|---|---|
object | string | |
campaigns | objecttotal sending | |
senders | objecttotal active | |
leads | objecttotal | |
replies | objecttotal unhandled | |
emails | objectsent_all_time sent_today delivered_all_time | |
engagement | objectreplied interested bounced hard_bounced blocked leads_reached auto_replied | Added up across campaigns, counted as each campaign's stats are: people who replied (out-of-office and auto-replies left out) and were interested; emails that bounced. |
Errors
Errors are JSON with the same shape throughout: { "error": { "code": "invalid_request", "message": "Each lead needs a valid email address.", "param": "leads.3.email" } }. The message is plain words, safe to show a person; param names the field that failed.
| Status | What it means |
|---|---|
400 | The body or a parameter isn't valid — error.param names the field. A campaign that can't start yet lists what's missing in error.blockers. |
401 | Missing, invalid or revoked API key. |
403 | read_only_key: a read-only key tried to make a change. plan_required: the API is a Pro and Scale feature. |
404 | No such resource in your workspace. |
409 | It can't happen right now: a campaign that isn't sending, the same reply within a minute (duplicate_reply), an address another lead uses (email_taken), or a request with the same Idempotency-Key still running. |
413 | The request body is over 5 MB. |
422 | The Idempotency-Key was already used for a different request. |
429 | More than 120 requests in a minute from one key. |
502 | The mail server refused a reply; the message says why in plain words. |