Docs
Everything here works with an API key. Every request and response on this page is a real call, made against a test workspace with made-up data.
Send requests to https://craftnudge.com/api. Make a key in Settings, on the API keys tab. It is shown once, so copy it then. Send it in the Authorization header as a bearer token.
A key acts for its whole workspace. It cannot change sign-in details, and the few settings only an owner can change refuse it.
Bodies are JSON. The lead and blocklist imports take a CSV instead, as the body with Content-Type text/csv, or as a file field in a multipart form.
A request that fails answers with a 4xx status and a JSON body saying why. A missing or unknown key gets 401. Lists of records in the responses below are cut to two, and CSV files to two rows. Where a call needs a service outside CraftNudge, a model, a Google Sheet or a DNS lookup, that service's answer was made up for the example too.
The mailboxes you send from. A password is stored encrypted and never comes back.
GET /mailboxes
Every inbox in the workspace, with what it has sent today and what it may send today.
Request
curl https://craftnudge.com/api/mailboxes \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "86b394fc-4eef-41ac-bc0d-be1e4d4c7414",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "hey@getlumen.example",
"fromName": "Sam from Lumen",
"smtpHost": "smtp.gmail.com",
"smtpPort": 465,
"smtpSecure": true,
"imapHost": "imap.gmail.com",
"imapPort": 993,
"username": "hey@getlumen.example",
"dailyCap": 40,
"rampStart": 10,
"rampIncrement": 5,
"rampStartedAt": "2026-09-08",
"minGapMinutes": 9,
"gapJitterMinutes": 5,
"signature": null,
"tags": [
"warming"
],
"domainDailyCap": null,
"status": "active",
"lastError": null,
"backoffUntil": null,
"createdAt": "2026-09-08T10:00:00.000Z",
"sentToday": 0,
"capToday": 40,
"rampShare": 5
},
{
"id": "498bb0fc-09a5-47c7-aa7e-64dd94d6c2ce",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "outreach@acme.example",
"fromName": "Jordan at Acme",
"smtpHost": "smtp.gmail.com",
"smtpPort": 465,
"smtpSecure": true,
"imapHost": "imap.gmail.com",
"imapPort": 993,
"username": "outreach@acme.example",
"dailyCap": 40,
"rampStart": 10,
"rampIncrement": 5,
"rampStartedAt": "2026-08-21",
"minGapMinutes": 9,
"gapJitterMinutes": 5,
"signature": null,
"tags": [
"uk"
],
"domainDailyCap": 60,
"status": "active",
"lastError": null,
"backoffUntil": null,
"createdAt": "2026-08-21T10:00:00.000Z",
"sentToday": 0,
"capToday": 40,
"rampShare": 6
}
]POST /mailboxes
Saves the SMTP and IMAP details and starts the inbox at 10 a day, adding 5 a day up to its cap. It does not log in. Test the connection from the Inboxes page before sending. Your plan’s inbox count applies.
Request
curl -X POST https://craftnudge.com/api/mailboxes \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "new@acme.example",
"from_name": "Jordan at Acme",
"smtp_host": "smtp.gmail.com",
"smtp_port": 465,
"smtp_secure": true,
"imap_host": "imap.gmail.com",
"imap_port": 993,
"username": "new@acme.example",
"password": "your-app-password",
"daily_cap": 30
}'Response, 200
{
"id": "078eb811-6d54-4824-84aa-c819a4974d84",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "new@acme.example",
"fromName": "Jordan at Acme",
"smtpHost": "smtp.gmail.com",
"smtpPort": 465,
"smtpSecure": true,
"imapHost": "imap.gmail.com",
"imapPort": 993,
"username": "new@acme.example",
"dailyCap": 30,
"rampStart": 10,
"rampIncrement": 5,
"rampStartedAt": "2026-09-14",
"minGapMinutes": 9,
"gapJitterMinutes": 5,
"signature": null,
"tags": [],
"domainDailyCap": null,
"status": "active",
"lastError": null,
"backoffUntil": null,
"createdAt": "2026-09-14T22:17:33.094Z"
}POST /mailboxes/:id/pause
Stops it sending. What it has sent today keeps counting.
Request
curl -X POST https://craftnudge.com/api/mailboxes/078eb811-6d54-4824-84aa-c819a4974d84/pause \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "paused"
}POST /mailboxes/:id/resume
Undoes a pause. An inbox its bounce rate stopped has to pass the connection test instead.
Request
curl -X POST https://craftnudge.com/api/mailboxes/078eb811-6d54-4824-84aa-c819a4974d84/resume \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "active"
}PATCH /mailboxes/:id
Any of the fields it was connected with, its caps and the gap between sends. Moving it to another server, port or username needs the password again.
Request
curl -X PATCH https://craftnudge.com/api/mailboxes/078eb811-6d54-4824-84aa-c819a4974d84 \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from_name": "Jordan Reyes",
"daily_cap": 40,
"min_gap_minutes": 6
}'Response, 200
{
"id": "078eb811-6d54-4824-84aa-c819a4974d84",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "new@acme.example",
"fromName": "Jordan Reyes",
"smtpHost": "smtp.gmail.com",
"smtpPort": 465,
"smtpSecure": true,
"imapHost": "imap.gmail.com",
"imapPort": 993,
"username": "new@acme.example",
"dailyCap": 40,
"rampStart": 10,
"rampIncrement": 5,
"rampStartedAt": "2026-09-14",
"minGapMinutes": 6,
"gapJitterMinutes": 5,
"signature": null,
"tags": [],
"domainDailyCap": null,
"status": "active",
"lastError": null,
"backoffUntil": null,
"createdAt": "2026-09-14T22:17:33.094Z"
}POST /mailboxes/bulk
The same change to every inbox in ids: daily_cap, domain_daily_cap or min_gap_minutes, or a tag with add_tag or remove_tag.
Request
curl -X POST https://craftnudge.com/api/mailboxes/bulk \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"078eb811-6d54-4824-84aa-c819a4974d84",
"86b394fc-4eef-41ac-bc0d-be1e4d4c7414"
],
"add_tag": "q4"
}'Response, 200
{
"changed": 2
}GET /mailboxes/scores
Each inbox’s score out of 100, the points it lost for bounces, replies, placement and errors, and the counts behind them.
Request
curl https://craftnudge.com/api/mailboxes/scores \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"mailbox_id": "86b394fc-4eef-41ac-bc0d-be1e4d4c7414",
"score": 50,
"parts": {
"bounces": 40,
"replies": 0,
"placement": 10,
"errors": 0
},
"inputs": {
"sent": 24,
"failed": 0,
"bounced": 2,
"replied": 2,
"placement": 0.6666666666666666
},
"enough_sends": true
},
{
"mailbox_id": "078eb811-6d54-4824-84aa-c819a4974d84",
"score": 100,
"parts": {
"bounces": 0,
"replies": 0,
"placement": 0,
"errors": 0
},
"inputs": {
"sent": 0,
"failed": 0,
"bounced": 0,
"replied": 0,
"placement": null
},
"enough_sends": false
}
]GET /mailboxes/domain-health
SPF, DKIM, DMARC, MX and the Spamhaus blocklists for every domain the workspace sends from. Each check is ok, warn, fail or unknown, with a fix for anything not ok. selector adds a DKIM selector to try. Only a few checks are allowed in a short time.
Request
curl https://craftnudge.com/api/mailboxes/domain-health \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"checked_at": "2026-09-14T22:17:33.137Z",
"domains": [
{
"domain": "acme.example",
"checks": [
{
"key": "spf",
"label": "SPF",
"status": "ok",
"detail": "v=spf1 include:_spf.google.com ~all",
"fix": null,
"value": "v=spf1 include:_spf.google.com ~all"
},
{
"key": "dkim",
"label": "DKIM",
"status": "ok",
"detail": "Signing key found at selector \"google\".",
"fix": null,
"value": "google"
}
]
},
{
"domain": "getlumen.example",
"checks": [
{
"key": "spf",
"label": "SPF",
"status": "ok",
"detail": "v=spf1 include:_spf.google.com ~all",
"fix": null,
"value": "v=spf1 include:_spf.google.com ~all"
},
{
"key": "dkim",
"label": "DKIM",
"status": "fail",
"value": null,
"detail": "No signing key at \"google\", \"selector1\", \"selector2\".",
"fix": "Turn on DKIM where the inbox lives, then publish the record it gives you. Google Workspace: Admin console, Apps, Gmail, Authenticate email. Microsoft 365: Defender portal, Email authentication, DKIM. If your provider uses another selector, type it in and check again."
}
]
}
]
}DELETE /mailboxes/:id
Refused with 409 while the inbox has sent in the last day, because that mail still counts against its domain. Pause it and delete it the next day.
Request
curl -X DELETE https://craftnudge.com/api/mailboxes/078eb811-6d54-4824-84aa-c819a4974d84 \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}A campaign is a draft until it is launched. Send only the fields you want to change.
POST /campaigns
A draft in the workspace’s timezone with the default schedule: weekdays, 09:00 to 17:00, 30 a day.
Request
curl -X POST https://craftnudge.com/api/campaigns \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "October follow-up"
}'Response, 200
{
"id": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "October follow-up",
"status": "draft",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 30,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": null,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": false,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": null,
"abOptimizeAfter": 0,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-14T22:17:33.147Z"
}GET /campaigns
Every campaign in the workspace, oldest first.
Request
curl https://craftnudge.com/api/campaigns \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "Q3 · Founder outreach",
"status": "active",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 40,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": 2400,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": true,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": "reply",
"abOptimizeAfter": 40,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-01T10:00:00.000Z"
},
{
"id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "Agency partners, UK",
"status": "active",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 20,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": 6000,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": false,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": null,
"abOptimizeAfter": 0,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-05T10:00:00.000Z"
}
]PATCH /campaigns/:id
Any of its settings, and mailbox_ids for the inboxes it sends from. windows takes up to four sending windows, each with days (0 is Sunday) and a start and end as HH:MM in the campaign’s timezone, and replaces days, window_start and window_end until it is set back to null. A window that runs past midnight, or overlaps another on the same day, is refused with 400, and so is changing days or hours while windows are set. lead_timezone reads the days and hours in each lead’s own timezone where the lead has one. A timezone that isn’t a real zone name is refused with 400.
Request
curl -X PATCH https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"window_start": "09:00",
"window_end": "17:00",
"daily_limit": 40,
"mailbox_ids": [
"86b394fc-4eef-41ac-bc0d-be1e4d4c7414"
]
}'Response, 200
{
"id": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "October follow-up",
"status": "draft",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 40,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": null,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": false,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": null,
"abOptimizeAfter": 0,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-14T22:17:33.147Z"
}PUT /campaigns/:id/steps
Replaces every step. Positions run from 1 with no gaps, and wait_minutes counts from the step before. A follow-up with an empty subject replies in the same thread.
Request
curl -X PUT https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/steps \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"position": 1,
"wait_minutes": 0,
"variants": [
{
"subject": "Quick question about {{company}}",
"body": "Hi {{first_name|there}},\n\nHow are you handling returns at {{company}}?\n\nJordan\n\nNot relevant? {{unsubscribe}}"
}
]
},
{
"position": 2,
"wait_minutes": 4320,
"variants": [
{
"subject": "",
"body": "Bumping this in case it got buried, {{first_name|there}}."
}
]
}
]'Response, 200
{
"steps": 2
}POST /sequence-templates
Saves the steps of campaign_id as they are now, under name, for the whole workspace. A campaign with no steps is refused with 400, and a name already used with 409.
Request
curl -X POST https://craftnudge.com/api/sequence-templates \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Two-step returns intro",
"campaign_id": "dc51bb22-00e5-44dc-9b9f-db781f5d094b"
}'Response, 200
{
"id": "b3a6d23a-872f-466d-a7fa-bbffff3cd0f4",
"name": "Two-step returns intro",
"steps": 2
}GET /sequence-templates
Every template in the workspace by name, with how many steps each has.
Request
curl https://craftnudge.com/api/sequence-templates \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "b3a6d23a-872f-466d-a7fa-bbffff3cd0f4",
"name": "Two-step returns intro",
"steps": 2,
"created_at": "2026-09-14T22:17:34.889Z"
}
]POST /campaigns/:id/steps/from-template
Replaces every step of the campaign with the template’s, checked by the same rules as setting the steps. Refused with 409 once the campaign has sent anything.
Request
curl -X POST https://craftnudge.com/api/campaigns/7649aabc-f62a-4a78-b4a5-3f61f419bb18/steps/from-template \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "b3a6d23a-872f-466d-a7fa-bbffff3cd0f4"
}'Response, 200
{
"steps": 2
}DELETE /sequence-templates/:id
Removes it from the workspace. Campaigns that used it keep their steps.
Request
curl -X DELETE https://craftnudge.com/api/sequence-templates/b3a6d23a-872f-466d-a7fa-bbffff3cd0f4 \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}GET /campaigns/:id
The campaign with its steps and the ids of the inboxes it sends from.
Request
curl https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"id": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "October follow-up",
"status": "draft",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 40,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": null,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": false,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": null,
"abOptimizeAfter": 0,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-14T22:17:33.147Z",
"steps": [
{
"id": "8e197d38-2e20-4a49-b893-ecd2a132550f",
"campaignId": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"position": 1,
"waitMinutes": 0,
"variants": [
{
"body": "Hi {{first_name|there}},\n\nHow are you handling returns at {{company}}?\n\nJordan\n\nNot relevant? {{unsubscribe}}",
"subject": "Quick question about {{company}}"
}
],
"task": null,
"condition": null
},
{
"id": "244d128e-766e-4c41-b24e-810eb65e5d1e",
"campaignId": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"position": 2,
"waitMinutes": 4320,
"variants": [
{
"body": "Bumping this in case it got buried, {{first_name|there}}.",
"subject": ""
}
],
"task": null,
"condition": null
}
],
"mailbox_ids": [
"86b394fc-4eef-41ac-bc0d-be1e4d4c7414"
]
}POST /campaigns/:id/leads/manual
Each lead is an email and any fields. Every lead goes through the import rules, and the answer counts where each one went.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/leads/manual \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"email": "ana@brightside.example",
"vars": {
"first_name": "Ana",
"company": "Brightside Home"
}
},
{
"email": "leo@tallgrass.example",
"vars": {
"first_name": "Leo",
"company": "Tallgrass Tea"
}
}
]
}'Response, 200
{
"inserted": 2,
"duplicates": 0,
"blocklisted": 0,
"invalid": 0,
"in_another_campaign": 0,
"contacted_recently": 0
}POST /campaigns/:id/leads
The same rules, from a CSV with an email column. Every other column becomes a field. A timezone column, or else a country column, sets the lead’s timezone, the same for every way leads are added.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/leads \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \ -H "Content-Type: text/csv" \ --data-binary @leads.csv
leads.csv
email,first_name,company ivy@northfield.example,Ivy,Northfield Linen sol@redwing.example,Sol,Redwing Bikes ana@brightside.example,Ana,Brightside Home
Response, 200
{
"inserted": 2,
"duplicates": 1,
"blocklisted": 0,
"invalid": 0,
"in_another_campaign": 0,
"contacted_recently": 0
}POST /campaigns/:id/leads/sheet
url is the link from File, Share, Publish to web in Google Sheets, with CSV chosen. The sheet is read once, only from Google, and every row goes through the same rules as a CSV.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/leads/sheet \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://docs.google.com/spreadsheets/d/e/2PACX-1vDocsExampleSheet/pub?output=csv"
}'Response, 200
{
"inserted": 2,
"duplicates": 0,
"blocklisted": 0,
"invalid": 0,
"in_another_campaign": 0,
"contacted_recently": 0
}GET /campaigns/:id/leads
The campaign’s leads with their status, step and fields, and provider: who receives the address’s mail, read from its domain’s MX, as google, microsoft, other, none or unknown, and null until it has been looked up. Filter with status, interest, verification (or unchecked), provider (or unchecked), list_id for leads on one of your lead lists, and q to search the email. An empty value is no filter.
Request
curl https://craftnudge.com/api/campaigns/5fdb0cdd-bc45-4f35-94bc-8bdc89f6f506/leads \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "b2fbb892-25c2-4b76-9b66-b11668bb2b1d",
"workspaceId": "b03dfd0e-6592-4278-98f3-530d2140a90a",
"campaignId": "5fdb0cdd-bc45-4f35-94bc-8bdc89f6f506",
"email": "ana@brightside.example",
"vars": {
"company": "Brightside Home",
"first_name": "Ana"
},
"timezone": null,
"status": "pending",
"stopReason": null,
"interest": null,
"wonAt": null,
"provider": "none",
"verification": null,
"verifiedAt": null,
"currentStep": 0,
"nextDueAt": null,
"awayUntil": null,
"createdAt": "2026-09-23T12:49:52.868Z",
"sendZone": "Asia/Kolkata"
},
{
"id": "e390a30c-e4db-42a6-9716-d33da6fba724",
"workspaceId": "b03dfd0e-6592-4278-98f3-530d2140a90a",
"campaignId": "5fdb0cdd-bc45-4f35-94bc-8bdc89f6f506",
"email": "bo@northpeak.example",
"vars": {
"company": "Northpeak Outdoor",
"first_name": "Bo"
},
"timezone": null,
"status": "pending",
"stopReason": null,
"interest": null,
"wonAt": null,
"provider": "none",
"verification": null,
"verifiedAt": null,
"currentStep": 0,
"nextDueAt": null,
"awayUntil": null,
"createdAt": "2026-09-23T12:49:52.868Z",
"sendZone": "Asia/Kolkata"
}
]POST /lead-filters
Saves filter under name for the whole workspace, with the same fields as listing a campaign’s leads. A field or value the leads list does not take, or an empty filter, is refused with 400, and a name already used with 409.
Request
curl -X POST https://craftnudge.com/api/lead-filters \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Risky, still to send",
"filter": {
"verification": "risky",
"status": "pending"
}
}'Response, 200
{
"id": "907e0b8c-3cf5-45e8-9798-1395b437e979",
"name": "Risky, still to send",
"filter": {
"status": "pending",
"verification": "risky"
}
}GET /lead-filters
Every saved filter in the workspace by name. Pass a filter’s fields to the leads list to apply it.
Request
curl https://craftnudge.com/api/lead-filters \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "907e0b8c-3cf5-45e8-9798-1395b437e979",
"name": "Risky, still to send",
"filter": {
"status": "pending",
"verification": "risky"
},
"created_at": "2026-09-14T22:17:34.934Z"
}
]DELETE /lead-filters/:id
Removes it from the workspace.
Request
curl -X DELETE https://craftnudge.com/api/lead-filters/907e0b8c-3cf5-45e8-9798-1395b437e979 \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}GET /campaigns/:id/leads.csv
Every lead in the campaign with its status, interest, step, stop reason, dates and fields, as a CSV file.
Request
curl https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/leads.csv \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
email,status,interest,current_step,stop_reason,created_at,last_sent_at,replied_at,next_due_at,company,first_name ana@brightside.example,pending,,0,,2026-09-14T22:17:33.179Z,,,,Brightside Home,Ana leo@tallgrass.example,pending,,0,,2026-09-14T22:17:33.179Z,,,,Tallgrass Tea,Leo
POST /campaigns/:id/preview
The steps you send, rendered as a lead would get them: fields filled from lead_id, or from one of the campaign’s leads, and the sender from one of its inboxes. Nothing is saved or sent.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/preview \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"steps": [
{
"subject": "Quick question about {{company}}",
"body": "Hi {{first_name|there}},\n\nHow are you handling returns at {{company}}?\n\nJordan\n\nNot relevant? {{unsubscribe}}"
},
{
"subject": "",
"body": "Bumping this in case it got buried, {{first_name|there}}."
}
]
}'Response, 200
{
"lead": {
"id": "56b5895f-e32e-4b7f-b5fa-ed26e0778d06",
"email": "ana@brightside.example"
},
"from": "Sam from Lumen <hey@getlumen.example>",
"steps": [
{
"subject": "Quick question about Brightside Home",
"text": "Hi Ana,\n\nHow are you handling returns at Brightside Home?\n\nJordan\n\nNot relevant? https://app.craftnudge.example/u/<token>"
},
{
"subject": "Re: Quick question about Brightside Home",
"text": "Bumping this in case it got buried, Ana."
}
]
}GET /campaigns/:id/merge-check
Each merge field in the saved steps that would come out blank, and for how many leads or inboxes. An empty list means none would.
Request
curl https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/merge-check \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[]
GET /campaigns/:id/copy-check
Warnings about the saved steps: spam words, more than two links, capitals, exclamation marks, a long subject or body, images, and no way to opt out. They never stop a campaign.
Request
curl https://craftnudge.com/api/campaigns/7de94972-f78e-4e08-9b90-733605d0541e/copy-check \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"position": 1,
"variant": 0,
"code": "spam_words",
"message": "Words that spam filters notice: click here, free, no obligation, 100%.",
"terms": [
"click here",
"free",
"no obligation",
"100%"
]
},
{
"position": 1,
"variant": 0,
"code": "links",
"message": "3 links. Cold email with more than 2 is more likely to land in spam.",
"terms": [
"https://acme.example/case-studies",
"https://acme.example/book",
"https://acme.example/pricing"
]
}
]POST /campaigns/:id/verify
Looks up whether the domain of each lead still due mail has a mail server, and marks the leads on domains without one as invalid, so nothing is sent to them. Free.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/verify \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"domains_checked": 7,
"bad_domains": 7,
"leads_checked": 7,
"marked_invalid": 7
}POST /campaigns/:id/verify-addresses
Sends each lead still due mail that has not been checked to the workspace’s verifier, and holds it from sending while it waits, for a day at most. marked is how many will be checked. Refused with 400 until an owner saves a verification key in Settings.
Request
curl -X POST https://craftnudge.com/api/campaigns/b6e6f2bc-177e-454c-ae89-2315e526e7ef/verify-addresses \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"marked": 9
}GET /campaigns/:id/verification
How many of the campaign’s leads came back valid, invalid, risky or unknown, how many are waiting, how many waited too long and went unchecked, how many were never checked, and how many were skipped as invalid, as risky and as catch-all.
Request
curl https://craftnudge.com/api/campaigns/b6e6f2bc-177e-454c-ae89-2315e526e7ef/verification \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"pending": 0,
"valid": 6,
"invalid": 1,
"risky": 2,
"unknown": 0,
"expired": 0,
"unchecked": 5,
"skipped_invalid": 1,
"skipped_risky": 2,
"skipped_catch_all": 1
}POST /campaigns/:id/activate
Needs at least one step, an inbox, and the one-click unsubscribe header on. Sending starts inside the next window.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/activate \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "active",
"warnings": []
}POST /campaigns/:id/pause
Stops new sends. Launching again carries on where it stopped.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/pause \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "paused"
}GET /campaigns/:id/stats
Sent, opened, clicked, replied and bounced, positive replies (marked interested, meeting booked, meeting completed or won), the leads by status, each step and variant, and each day. Replies to a one-off email are not counted.
Request
curl https://craftnudge.com/api/campaigns/36b04d47-3648-4f76-9ea6-e18b41cc9eaf/stats \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"variants": [
{
"step": 1,
"variant": 0,
"sent": 12,
"replies": 3,
"positive": 2,
"reply_rate": 25
},
{
"step": 1,
"variant": 1,
"sent": 12,
"replies": 2,
"positive": 1,
"reply_rate": 16.7
}
],
"by_day": [
{
"day": "2026-09-02",
"sent": 3,
"replies": 0,
"bounces": 0
},
{
"day": "2026-09-03",
"sent": 3,
"replies": 3,
"bounces": 0
}
],
"leads": {
"total": 36,
"completed": 6,
"in_sequence": 8,
"pending": 8,
"replied": 13,
"unsubscribed": 1
},
"sent": 43,
"one_off_sent": 0,
"opened": 19,
"clicked": 3,
"replied": 6,
"positive": 3,
"bounced": 3,
"auto_replies": 2,
"unsubscribed": 1
}POST /campaigns/:id/duplicate
A new draft named with (copy), with the same settings, steps and inboxes, and no leads.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/duplicate \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"id": "f4383386-616a-4197-b82a-14877a116086",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "October follow-up (copy)",
"status": "draft",
"timezone": "Europe/London",
"days": [
1,
2,
3,
4,
5
],
"windowStart": "09:00",
"windowEnd": "17:00",
"windows": null,
"leadTimezone": false,
"dailyLimit": 40,
"maxNewLeadsPerDay": null,
"prioritizeNew": false,
"skipRisky": true,
"dealValue": null,
"stopOnReply": true,
"stopOnAutoReply": false,
"openTracking": false,
"clickTracking": false,
"unsubHeader": true,
"textOnly": false,
"companyReplyStop": false,
"providerMatching": false,
"abWinnerMetric": null,
"abOptimizeAfter": 0,
"cc": null,
"bcc": null,
"tags": [],
"startDate": null,
"endDate": null,
"createdAt": "2026-09-14T22:17:33.249Z"
}DELETE /campaigns/:id
Deletes the campaign with its steps, its leads and its sends.
Request
curl -X DELETE https://craftnudge.com/api/campaigns/f4383386-616a-4197-b82a-14877a116086 \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}A list keeps people outside any campaign, so the same set can start more than one.
POST /lead-lists
An empty list with a name.
Request
curl -X POST https://craftnudge.com/api/lead-lists \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "November shops"
}'Response, 200
{
"id": "a1bc2d84-a52a-40d1-b7ca-e4a9703f0cfd",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"name": "November shops",
"createdAt": "2026-09-14T22:17:33.258Z"
}POST /lead-lists/:id/leads
A list only checks that each row is an address and new to the list. The campaign rules run when a list starts a campaign.
Request
curl -X POST https://craftnudge.com/api/lead-lists/a1bc2d84-a52a-40d1-b7ca-e4a9703f0cfd/leads \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \ -H "Content-Type: text/csv" \ --data-binary @leads.csv
leads.csv
email,first_name,company wren@larchmont.example,Wren,Larchmont Candles juno@foxhall.example,Juno,Foxhall Prints not-an-address,,
Response, 200
{
"added": 2,
"duplicates": 0,
"invalid": 1
}GET /lead-lists
Every list with how many people it holds.
Request
curl https://craftnudge.com/api/lead-lists \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "23d191f7-8727-46f1-b905-6a6a2e1ffe9e",
"name": "UK Woo shops, September",
"created_at": "2026-09-10T10:00:00.000Z",
"leads": 39
},
{
"id": "ac12d76a-bb14-4995-ae03-0de273616901",
"name": "Agency owners",
"created_at": "2026-09-12T10:00:00.000Z",
"leads": 17
}
]POST /campaigns/:id/leads/from-list
Adds the list’s people to the campaign through the import rules.
Request
curl -X POST https://craftnudge.com/api/campaigns/dc51bb22-00e5-44dc-9b9f-db781f5d094b/leads/from-list \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"list_id": "a1bc2d84-a52a-40d1-b7ca-e4a9703f0cfd"
}'Response, 200
{
"inserted": 2,
"duplicates": 0,
"blocklisted": 0,
"invalid": 0,
"in_another_campaign": 0,
"contacted_recently": 0
}DELETE /lead-lists/:id
Deletes the list and who is on it. Leads it already added to a campaign stay in that campaign.
Request
curl -X DELETE https://craftnudge.com/api/lead-lists/a1bc2d84-a52a-40d1-b7ca-e4a9703f0cfd \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}One person in one campaign.
GET /leads
Leads across the whole workspace, newest first. q matches part of the address.
Request
curl "https://craftnudge.com/api/leads?q=maya" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "57931739-2a3c-4654-b87e-b43edf3077f5",
"email": "maya@northwind.example",
"vars": {
"domain": "northwind.example",
"company": "Northwind Interiors",
"first_name": "Maya"
},
"status": "replied",
"stopReason": "replied",
"interest": "meeting_booked",
"currentStep": 1,
"nextDueAt": null,
"createdAt": "2026-09-02T10:00:00.000Z",
"campaign_id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"campaign": "Q3 · Founder outreach"
}
]GET /leads/:id
One lead with its status, step and fields.
Request
curl https://craftnudge.com/api/leads/57931739-2a3c-4654-b87e-b43edf3077f5 \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"id": "57931739-2a3c-4654-b87e-b43edf3077f5",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"campaignId": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"email": "maya@northwind.example",
"vars": {
"domain": "northwind.example",
"company": "Northwind Interiors",
"first_name": "Maya"
},
"timezone": null,
"status": "replied",
"stopReason": "replied",
"interest": "meeting_booked",
"verification": null,
"verifiedAt": null,
"currentStep": 1,
"nextDueAt": null,
"awayUntil": null,
"createdAt": "2026-09-02T10:00:00.000Z"
}PATCH /leads/:id
Set interest to a label, status to paused or active, or timezone to a zone name such as Europe/London, or null. Only a paused lead can be made active again.
Request
curl -X PATCH https://craftnudge.com/api/leads/57931739-2a3c-4654-b87e-b43edf3077f5 \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"interest": "meeting_booked"
}'Response, 200
{
"id": "57931739-2a3c-4654-b87e-b43edf3077f5",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"campaignId": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"email": "maya@northwind.example",
"vars": {
"domain": "northwind.example",
"company": "Northwind Interiors",
"first_name": "Maya"
},
"timezone": null,
"status": "replied",
"stopReason": "replied",
"interest": "meeting_booked",
"verification": null,
"verifiedAt": null,
"currentStep": 1,
"nextDueAt": null,
"awayUntil": null,
"createdAt": "2026-09-02T10:00:00.000Z"
}GET /leads/:id/activity
Everything that went to the lead and everything that came back, in order.
Request
curl https://craftnudge.com/api/leads/57931739-2a3c-4654-b87e-b43edf3077f5/activity \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"lead": {
"id": "57931739-2a3c-4654-b87e-b43edf3077f5",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"campaignId": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"email": "maya@northwind.example",
"vars": {
"domain": "northwind.example",
"company": "Northwind Interiors",
"first_name": "Maya"
},
"timezone": null,
"status": "replied",
"stopReason": "replied",
"interest": "meeting_booked",
"verification": null,
"verifiedAt": null,
"currentStep": 1,
"nextDueAt": null,
"awayUntil": null,
"createdAt": "2026-09-02T10:00:00.000Z"
},
"timeline": [
{
"type": "sent",
"at": "2026-09-02T09:00:00.000Z",
"step": 1,
"step_label": "Step 1",
"detail": "\"Quick question about Northwind Interiors\" via outreach@acme.example",
"variant": 0
},
{
"type": "reply",
"at": "2026-09-03T05:00:00.000Z",
"step": null,
"step_label": null,
"detail": "Re: Quick question about Northwind Interiors: Yes, this is a real problem for us. Thursday 3pm works if you can do it.",
"variant": null
}
]
}POST /leads/one-off
The lead to send a one-off email to, for an address typed by hand. An address already a lead in the workspace comes back as that lead, with created false. A new one is added to a draft campaign named One-off with no steps, so the blocklist and dedupe apply to it. An address that is blocklisted, or that unsubscribed or bounced as any lead, is refused with 400.
Request
curl -X POST https://craftnudge.com/api/leads/one-off \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "hello@larchwood.example"
}'Response, 200
{
"lead_id": "fef6f224-818a-4fab-aa49-b7ff439c83cd",
"created": true
}POST /leads/:id/send
Queues one email from mailboxId: a new email with subject, or with replyTo, the id of the lead’s reply, an answer in that thread. Nothing goes out from the call. It goes when the lead’s campaign window is open and the inbox has room under its caps, and scheduled_for is the earliest that can be. The same message queued twice is one email. If the lead replies, unsubscribes or bounces before it goes, it is dropped.
Request
curl -X POST https://craftnudge.com/api/leads/40a551f9-2759-4b99-8d48-2a9fab2372be/send \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mailboxId": "86b394fc-4eef-41ac-bc0d-be1e4d4c7414",
"subject": "The returns numbers",
"body": "Hi,\n\nYou asked about returns last month. Here are the numbers from shops your size.\n\nJordan"
}'Response, 200
{
"id": "11bd7bc8-498e-464d-a481-73231b6f8ae6",
"status": "queued",
"scheduled_for": "2026-09-15T08:00:00.000Z"
}GET /leads/search
Leads whose address contains q, at least two characters, across every campaign, each with its campaign. Up to limit, 20 by default.
Request
curl "https://craftnudge.com/api/leads/search?q=north" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "6c18287c-95c0-4ca8-ab43-f90cbcc233c8",
"email": "ade@northcote.example",
"status": "replied",
"stop_reason": "replied",
"current_step": 1,
"created_at": "2026-09-06T10:00:00.000Z",
"campaign_id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"campaign": "Agency partners, UK"
},
{
"id": "3644b52a-1110-4c81-87b1-18059d891d1b",
"email": "ivy@northfield.example",
"status": "pending",
"stop_reason": null,
"current_step": 0,
"created_at": "2026-09-14T22:17:33.191Z",
"campaign_id": "dc51bb22-00e5-44dc-9b9f-db781f5d094b",
"campaign": "October follow-up"
}
]DELETE /leads/:id
Only a lead that has never been emailed. One that has is refused, so it cannot be added again and mailed twice. Pause it instead.
Request
curl -X DELETE https://craftnudge.com/api/leads/bc6194f9-9f6a-4de0-857b-fd87d5fbce3f \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}What the unibox shows: replies, bounces and auto-replies, each matched to the send it answers.
GET /replies
Newest first. Filter with kind (reply, bounce or auto_reply), status, unread, campaign_id, mailbox_id and q.
Request
curl "https://craftnudge.com/api/replies?kind=reply&limit=2" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "5c4773ad-b16a-4a91-aee9-f10264c620d0",
"kind": "reply",
"status": "interested",
"is_read": false,
"replied_at": null,
"assisted_interest": null,
"assisted_confidence": null,
"from_email": "rosa@pixelforge.example",
"subject": "Re: Returns for the shops you build",
"snippet": "We have three clients who ask about this every month. What does the referral look like?",
"body_text": "We have three clients who ask about this every month. What does the referral look like?\n\nRosa",
"campaign_id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"mailbox_id": "86b394fc-4eef-41ac-bc0d-be1e4d4c7414",
"lead_id": "f2b2b504-7aab-4370-954a-83184844f2e6",
"received_at": "2026-09-10T12:00:00.000Z",
"lead_away_until": null,
"assigned_user_id": null,
"snoozed_until": null
},
{
"id": "c7a7d672-d95e-48d5-adbe-5780b349ed18",
"kind": "reply",
"status": "lead",
"is_read": false,
"replied_at": null,
"assisted_interest": null,
"assisted_confidence": null,
"from_email": "omar@quillstack.example",
"subject": "Re: Quick question about Quillstack Stationery",
"snippet": "Who else are you working with in furniture?",
"body_text": "Who else are you working with in furniture?\n\nOmar\nQuillstack Stationery",
"campaign_id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"mailbox_id": "498bb0fc-09a5-47c7-aa7e-64dd94d6c2ce",
"lead_id": "0c01d0ac-25f0-4a84-91af-7b65e50694f3",
"received_at": "2026-09-08T07:55:00.000Z",
"lead_away_until": null,
"assigned_user_id": null,
"snoozed_until": null
}
]GET /replies/counts
How many are unread, how many of those are replies, and how many have been answered.
Request
curl https://craftnudge.com/api/replies/counts \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"unread": 5,
"unread_replies": 5,
"answered": 2
}PATCH /replies/:id
Set status to an interest label, or is_read. The label is copied onto the lead.
Request
curl -X PATCH https://craftnudge.com/api/replies/5c4773ad-b16a-4a91-aee9-f10264c620d0 \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "interested"
}'Response, 200
{
"updated": true
}POST /replies/:id/reply
Sends body from the inbox that got the reply, in the same thread.
Request
curl -X POST https://craftnudge.com/api/replies/5c4773ad-b16a-4a91-aee9-f10264c620d0/reply \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Thanks. Thursday at 3pm works, I will send an invite."
}'Response, 200
{
"sent": true
}PATCH /replies/:id/assign
user_id is someone on the team, or null to take it off them.
Request
curl -X PATCH https://craftnudge.com/api/replies/5c4773ad-b16a-4a91-aee9-f10264c620d0/assign \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "1549d98d-8a73-492b-88dc-32e153790893"
}'Response, 200
{
"assigned_user_id": "1549d98d-8a73-492b-88dc-32e153790893"
}POST /replies/:id/unsubscribe
Unsubscribes the lead the reply came from, the same as the link in the email: no more mail, their address on the blocklist, and the unsubscribe event. The reply is marked not_interested. Calling it again changes nothing and fires no second event. 404 when the reply has no lead.
Request
curl -X POST https://craftnudge.com/api/replies/c7a7d672-d95e-48d5-adbe-5780b349ed18/unsubscribe \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"unsubscribed": true
}PATCH /replies/:id/snooze
until is a date and time. The reply leaves the list until then and comes back where it was. null brings it back now.
Request
curl -X PATCH https://craftnudge.com/api/replies/c7a7d672-d95e-48d5-adbe-5780b349ed18/snooze \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"until": "2026-09-21T09:00:00.000Z"
}'Response, 200
{
"snoozed_until": "2026-09-21T09:00:00.000Z"
}Answers written once and filled in for a thread. Sending one is an ordinary answer to a reply.
GET /saved-replies
Every saved reply in the workspace, by name.
Request
curl https://craftnudge.com/api/saved-replies \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "b1f1d379-0d0b-41df-9d3a-aa9f71a8378d",
"name": "Back from holiday",
"subject": null,
"body": "Hi {{firstName|there}},\n\nI hope the break was good. Is returns still something you want to look at this quarter?\n\n{{sendingAccountFirstName}}"
},
{
"id": "913df982-382b-446a-8ce8-59905a62c4ea",
"name": "Book a call",
"subject": null,
"body": "Hi {{firstName|there}},\n\nGreat to hear. Would a 15 minute call this week work? You can pick a time here: [add your booking link]\n\n{{sendingAccountFirstName}}"
}
]GET /saved-replies/examples
The three examples the empty Saved replies page offers. Nothing is saved.
Request
curl https://craftnudge.com/api/saved-replies/examples \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"name": "Not now",
"body": "Hi {{firstName|there}},\n\nThanks for letting me know. I will leave it for now and check back in a few months.\n\n{{sendingAccountFirstName}}"
},
{
"name": "Send information",
"body": "Hi {{firstName|there}},\n\nHere is a short overview of what we do and what it costs: [add your link]\n\nHappy to answer any questions.\n\n{{sendingAccountFirstName}}"
}
]POST /saved-replies
name and body, with merge fields as in a campaign step, and an optional subject for a new email. Names are unique in the workspace.
Request
curl -X POST https://craftnudge.com/api/saved-replies \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Back from holiday",
"body": "Hi {{firstName|there}},\n\nI hope the break was good. Is returns still something you want to look at this quarter?\n\n{{sendingAccountFirstName}}"
}'Response, 201
{
"id": "b1f1d379-0d0b-41df-9d3a-aa9f71a8378d",
"name": "Back from holiday",
"subject": null,
"body": "Hi {{firstName|there}},\n\nI hope the break was good. Is returns still something you want to look at this quarter?\n\n{{sendingAccountFirstName}}"
}PATCH /saved-replies/:id
Any of name, subject and body.
Request
curl -X PATCH https://craftnudge.com/api/saved-replies/b1f1d379-0d0b-41df-9d3a-aa9f71a8378d \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Back from a break"
}'Response, 200
{
"id": "b1f1d379-0d0b-41df-9d3a-aa9f71a8378d",
"name": "Back from a break",
"subject": null,
"body": "Hi {{firstName|there}},\n\nI hope the break was good. Is returns still something you want to look at this quarter?\n\n{{sendingAccountFirstName}}"
}POST /saved-replies/:id/render
The subject and body with merge fields filled in, for a reply with reply_id, or for a lead and an inbox with lead_id and mailbox_id. Nothing is sent.
Request
curl -X POST https://craftnudge.com/api/saved-replies/b1f1d379-0d0b-41df-9d3a-aa9f71a8378d/render \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reply_id": "5c4773ad-b16a-4a91-aee9-f10264c620d0"
}'Response, 200
{
"subject": null,
"body": "Hi Rosa,\n\nI hope the break was good. Is returns still something you want to look at this quarter?\n\nSam"
}DELETE /saved-replies/:id
Removes it for everyone in the workspace.
Request
curl -X DELETE https://craftnudge.com/api/saved-replies/b1f1d379-0d0b-41df-9d3a-aa9f71a8378d \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}Manual steps waiting on someone, such as a LinkedIn connection or a call. The sequence waits until the task is closed.
GET /tasks
Every task waiting on someone, the one due first at the top, with its lead and campaign. A task whose lead no longer fits its step does not show.
Request
curl https://craftnudge.com/api/tasks \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "c1902a40-88d4-45cd-81fb-1358bc21a298",
"kind": "linkedin_connect",
"text": "Connect with Femi on LinkedIn and mention the referral terms.",
"step_position": 2,
"due_at": "2026-09-14T19:17:15.635Z",
"campaign": {
"id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"name": "Agency partners, UK"
},
"lead": {
"id": "20b8ccc9-5d69-422f-bedd-9a35021a77b1",
"email": "femi@greenway.example",
"linkedin": null,
"phone": null,
"vars": {
"company": "Greenway Web Studio",
"first_name": "Femi"
}
}
},
{
"id": "96332202-d172-412b-8a7d-7dfcaac29fc3",
"kind": "linkedin_connect",
"text": "Connect with Kai on LinkedIn and mention the referral terms.",
"step_position": 2,
"due_at": "2026-09-14T20:17:15.635Z",
"campaign": {
"id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"name": "Agency partners, UK"
},
"lead": {
"id": "d674ae2b-7f37-4fce-9376-58b326ce2ee2",
"email": "kai@ashdown.example",
"linkedin": null,
"phone": null,
"vars": {
"company": "Ashdown Commerce",
"first_name": "Kai"
}
}
}
]POST /tasks/:id/done
Closes the task and moves the lead on to its next step, as an email would.
Request
curl -X POST https://craftnudge.com/api/tasks/c1902a40-88d4-45cd-81fb-1358bc21a298/done \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "done"
}POST /tasks/:id/skip
Closes the task without doing it. The lead moves on the same way, and the task keeps that it was skipped.
Request
curl -X POST https://craftnudge.com/api/tasks/96332202-d172-412b-8a7d-7dfcaac29fc3/skip \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"status": "skipped"
}Leads who replied with interest, by how far they got. A campaign’s deal_value, set with Change a campaign, is what each lead is worth.
GET /analytics/pipeline
The five stages from interested to lost, each with its count, its value and its leads, and the value won so far.
Request
curl https://craftnudge.com/api/analytics/pipeline \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"stages": [
{
"stage": "interested",
"count": 5,
"value": 15600,
"leads": [
{
"id": "f2b2b504-7aab-4370-954a-83184844f2e6",
"email": "rosa@pixelforge.example",
"campaign_id": "b6e6f2bc-177e-454c-ae89-2315e526e7ef",
"campaign": "Agency partners, UK",
"value": 6000
},
{
"id": "433c20dc-8d54-4208-9381-09623f7797db",
"email": "ben@pebblebrook.example",
"campaign_id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"campaign": "Q3 · Founder outreach",
"value": 2400
}
]
},
{
"stage": "meeting_booked",
"count": 3,
"value": 7200,
"leads": [
{
"id": "f8ba6bf0-85ec-4dc1-932d-6f0e7943da5f",
"email": "beth@willowmere.example",
"campaign_id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"campaign": "Q3 · Founder outreach",
"value": 2400
},
{
"id": "9c7f79fa-5697-401d-ad4c-7d25f0d8efef",
"email": "cal@dunmore.example",
"campaign_id": "36b04d47-3648-4f76-9ea6-e18b41cc9eaf",
"campaign": "Q3 · Founder outreach",
"value": 2400
}
]
}
],
"won_value": 8400
}PATCH /analytics/pipeline/:leadId
stage is meeting_booked, meeting_completed, won or lost. Nothing moves into interested from here, and only a lead who replied can move. Any other is refused with 400. A move sends nothing, stops any step still to come for that lead, and fires the interest_changed event.
Request
curl -X PATCH https://craftnudge.com/api/analytics/pipeline/f2b2b504-7aab-4370-954a-83184844f2e6 \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stage": "meeting_booked"
}'Response, 200
{
"id": "f2b2b504-7aab-4370-954a-83184844f2e6",
"stage": "meeting_booked"
}Addresses and whole domains that are never contacted. Checked on every import and again at the moment of sending.
POST /blocklist
Each entry is an address or a whole domain.
Request
curl -X POST https://craftnudge.com/api/blocklist \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"nobody@rival.example",
"spamtrap.example"
]
}'Response, 200
{
"added": 2
}POST /blocklist/import
A CSV of addresses and domains. An email or domain column is used when the file has one, otherwise the first column.
Request
curl -X POST https://craftnudge.com/api/blocklist/import \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \ -H "Content-Type: text/csv" \ --data-binary @blocklist.csv
blocklist.csv
email old@list.example stale.example not an address
Response, 200
{
"added": 2,
"already": 0,
"invalid": 1
}GET /blocklist
Every entry and why it is there: manual, import, unsubscribe or hard_bounce.
Request
curl https://craftnudge.com/api/blocklist \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "old@list.example",
"reason": "import",
"createdAt": "2026-09-14T22:17:33.580Z"
},
{
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"email": "stale.example",
"reason": "import",
"createdAt": "2026-09-14T22:17:33.580Z"
}
]DELETE /blocklist/:email
Removes one address or domain.
Request
curl -X DELETE https://craftnudge.com/api/blocklist/stale.example \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}The numbers behind the Analytics and Deliverability pages.
GET /reports/summary
Campaigns, inboxes, and everything sent, opened, replied and bounced so far.
Request
curl https://craftnudge.com/api/reports/summary \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"campaigns": 4,
"active_campaigns": 2,
"mailboxes": 4,
"sent": 53,
"opened": 19,
"replied": 7,
"bounced": 4,
"open_rate": 35.8,
"reply_rate": 13.2
}GET /reports/analytics
Totals, by day, by campaign and by inbox between from and to, written YYYY-MM-DD. The last 30 days when they are left out. campaign_id narrows every number to one campaign. started counts the leads whose first step went out. won counts the leads moved to won in the range, and won_value adds up their campaigns’ deal values. An email opened or clicked more than once counts once. open_rate and click_rate count only campaigns with that tracking on, and are null when none has it. campaigns lists each campaign that has sent, to narrow to.
Request
curl "https://craftnudge.com/api/reports/analytics?from=2026-08-24&to=2026-09-23" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"from": "2026-08-24T00:00:00.000Z",
"to": "2026-09-24T00:00:00.000Z",
"totals": {
"sent": 209,
"started": 123,
"replies": 15,
"bounces": 9,
"positive": 7,
"reply_rate": 7.2,
"bounce_rate": 4.3,
"opened": 48,
"clicked": 12,
"open_rate": 37.2,
"click_rate": 9.3,
"won": 3,
"won_value": 7500
},
"by_day": [
{
"day": "2026-08-24",
"sent": 21,
"replies": 0,
"bounces": 0,
"opened": 5,
"clicked": 2
},
{
"day": "2026-08-25",
"sent": 11,
"replies": 1,
"bounces": 0,
"opened": 5,
"clicked": 1
}
],
"by_campaign": [
{
"id": "7763dac2-afbf-4bf9-8ce9-61b456211582",
"label": "Founders, Q3",
"sent": 129,
"replies": 9,
"bounces": 7,
"positive": 4,
"reply_rate": 7,
"bounce_rate": 5.4
},
{
"id": "d8650d6d-d38a-4be7-9c92-7ab0f9704910",
"label": "Agencies, UK",
"sent": 80,
"replies": 6,
"bounces": 2,
"positive": 3,
"reply_rate": 7.5,
"bounce_rate": 2.5
}
],
"by_mailbox": [
{
"id": "0a42766f-a009-47a8-b23a-c8f4390768de",
"label": "maya@northwind.example",
"sent": 111,
"replies": 8,
"bounces": 5,
"positive": 5,
"reply_rate": 7.2,
"bounce_rate": 4.5
},
{
"id": "406ff00a-e450-429b-9335-97bc13d7fd26",
"label": "sam@northwind.example",
"sent": 98,
"replies": 7,
"bounces": 4,
"positive": 2,
"reply_rate": 7.1,
"bounce_rate": 4.1
}
],
"campaigns": [
{
"id": "d8650d6d-d38a-4be7-9c92-7ab0f9704910",
"name": "Agencies, UK"
},
{
"id": "7763dac2-afbf-4bf9-8ce9-61b456211582",
"name": "Founders, Q3"
}
]
}GET /reports/deliverability
The last 7 days: SMTP answers by code, bounces by cause, and each inbox’s sends, deferrals, rejections and answered replies.
Request
curl https://craftnudge.com/api/reports/deliverability \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"window_days": 7,
"smtp": {
"accepted": 34,
"deferred": 4,
"rejected": 2,
"by_code": {
"250": 34,
"421": 4,
"550": 2
}
},
"bounce_reasons": {
"bad_address": 3,
"blocked": 1
},
"per_inbox": [
{
"email": "hey@getlumen.example",
"sent": 14,
"deferred": 4,
"rejected": 0,
"answered": 1
},
{
"email": "outreach@acme.example",
"sent": 11,
"deferred": 0,
"rejected": 0,
"answered": 0
}
]
}GET /reports/analytics.csv
The same numbers between from and to, for one campaign with campaign_id, as a CSV file, a row for each campaign, mailbox or day, chosen with group.
Request
curl "https://craftnudge.com/api/reports/analytics.csv?group=campaign&from=2026-08-14&to=2026-09-13" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
campaign,sent,replies,reply_rate,positive,bounces,bounce_rate Q3 · Founder outreach,42,6,14.3,3,3,7.1 "Agency partners, UK",10,1,10,1,1,10
GET /reports/domains
Each sending domain’s sends for each day against what it was allowed, over the last days, 14 by default.
Request
curl "https://craftnudge.com/api/reports/domains?days=7" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"days": [
"2026-09-08",
"2026-09-09",
"2026-09-10",
"2026-09-11",
"2026-09-12",
"2026-09-13",
"2026-09-14"
],
"domains": [
{
"domain": "acme.example",
"inboxes": 2,
"domain_cap": 60,
"points": [
{
"day": "2026-09-08",
"sent": 4,
"allowed": 60
},
{
"day": "2026-09-09",
"sent": 5,
"allowed": 60
}
]
},
{
"domain": "getlumen.example",
"inboxes": 1,
"domain_cap": null,
"points": [
{
"day": "2026-09-08",
"sent": 1,
"allowed": 10
},
{
"day": "2026-09-09",
"sent": 3,
"allowed": 15
}
]
}
]
}GET /analytics/best-time
Replies over the last days, 90 by default, counted by weekday and hour in the workspace timezone. grid has a row for each weekday and a count for each hour.
Request
curl "https://craftnudge.com/api/analytics/best-time?days=30" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"timezone": "Europe/London",
"days": 30,
"replies": 7,
"grid": [
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]
]
}GET /reports/dmarc
A summary of the DMARC aggregate reports read over the last days, 30 by default: the messages, how many passed, and where they came from.
Request
curl "https://craftnudge.com/api/reports/dmarc?days=30" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"report_address": "dmarc@acme.example",
"window_days": 30,
"summary": {
"reports": 0,
"messages": 0,
"passed": 0,
"dkim_passed": 0,
"spf_passed": 0,
"quarantined": 0,
"rejected": 0,
"sources": []
},
"latest": []
}PATCH /reports/dmarc
report_address is the connected inbox your DMARC record sends reports to, so its mail is read as reports and not as replies. null turns it off.
Request
curl -X PATCH https://craftnudge.com/api/reports/dmarc \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"report_address": "dmarc@acme.example"
}'Response, 200
{
"report_address": "dmarc@acme.example"
}The weekly warmup ramp for each sending domain, and placement tests to seed addresses you own.
GET /domains
Each domain the workspace sends from, with its inboxes, its ramp week and the daily cap that week split across its active inboxes, why it is held back if it is, and its sends and bounces over 7 days.
Request
curl https://craftnudge.com/api/domains \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"domain": "acme.example",
"mailboxes": 2,
"active_mailboxes": 2,
"warmup_started_at": "2026-08-28",
"current_week": 3,
"weekly_cap": 12,
"per_mailbox_cap": [
6,
6
],
"held_back_reason": null,
"sent_7d": 20,
"bounced_7d": 0
},
{
"domain": "getlumen.example",
"mailboxes": 1,
"active_mailboxes": 1,
"warmup_started_at": "2026-09-08",
"current_week": 1,
"weekly_cap": 5,
"per_mailbox_cap": [
5
],
"held_back_reason": null,
"sent_7d": 14,
"bounced_7d": 1
}
]POST /domains
Starts the weekly warmup ramp today for a domain the workspace has an inbox on. A domain already on its ramp answers already.
Request
curl -X POST https://craftnudge.com/api/domains \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "tallpine.example"
}'Response, 200
{
"id": "8f4a1c82-50b7-434d-af0e-56f2035faadf",
"workspaceId": "998eec8c-7a1c-4a30-a6cf-6afb82f89677",
"domain": "tallpine.example",
"warmupStartedAt": "2026-09-14",
"currentWeek": 1,
"lastCheckedAt": "2026-09-14T22:17:33.649Z",
"heldBackReason": null,
"createdAt": "2026-09-14T22:17:33.653Z"
}DELETE /domains/:domain
Removes the domain’s ramp. Its inboxes stay.
Request
curl -X DELETE https://craftnudge.com/api/domains/tallpine.example \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}GET /seeds
The addresses you own that placement tests go to.
Request
curl https://craftnudge.com/api/seeds \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"email": "acme.seedbox@gmail.example",
"label": "Gmail"
},
{
"email": "acme.seedbox@outlook.example",
"label": "Outlook"
}
]POST /seeds
email, and a label such as Gmail. Use addresses you own, one on each provider you care about.
Request
curl -X POST https://craftnudge.com/api/seeds \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "acme.seedbox@yahoo.example",
"label": "Yahoo"
}'Response, 200
{
"added": true
}DELETE /seeds/:email
Takes one address off the seed list.
Request
curl -X DELETE https://craftnudge.com/api/seeds/acme.seedbox@yahoo.example \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}POST /placement-tests
Sends subject and body from mailbox_id to every seed address. These are real emails, counted against the inbox’s cap and its domain’s, and refused when either is spent.
Request
curl -X POST https://craftnudge.com/api/placement-tests \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mailbox_id": "86b394fc-4eef-41ac-bc0d-be1e4d4c7414",
"subject": "Quick question about returns",
"body": "Hi,\n\nHow are you handling returns this quarter?\n\nJordan"
}'Response, 200
{
"id": "d4933290-aab8-49d3-9706-79217da529df",
"sent_to": 3
}PATCH /placement-tests/:id
seed and placement: inbox, promotions, spam or missing, as you saw it in that inbox.
Request
curl -X PATCH https://craftnudge.com/api/placement-tests/d4933290-aab8-49d3-9706-79217da529df \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"seed": "acme.seedbox@gmail.example",
"placement": "inbox"
}'Response, 200
{
"recorded": true
}GET /placement-tests
The last 20 tests with what was recorded for each seed.
Request
curl https://craftnudge.com/api/placement-tests \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "d4933290-aab8-49d3-9706-79217da529df",
"subject": "Quick question about returns",
"created_at": "2026-09-14T22:17:33.717Z",
"mailbox": "hey@getlumen.example",
"results": [
{
"seed": "acme.seedbox@gmail.example",
"error": null,
"placement": "inbox"
},
{
"seed": "acme.seedbox@outlook.example",
"error": null,
"placement": null
}
]
},
{
"id": "abe5059b-2958-4b46-b942-95904e1c1acb",
"subject": "Quick question about Northwind Interiors",
"created_at": "2026-09-12T08:15:00.000Z",
"mailbox": "outreach@acme.example",
"results": [
{
"seed": "acme.seedbox@gmail.example",
"error": null,
"placement": "promotions"
},
{
"seed": "acme.seedbox@outlook.example",
"error": null,
"placement": "inbox"
}
]
}
]GET /placement-history
The last 50 tests, newest first, for one inbox with mailbox_id or for all. Each has how many seeds it went to, how many were recorded, the seeds still waiting for a result, and by provider how many landed in each tab. A provider is the seed’s label, or its domain when it has none.
Request
curl "https://craftnudge.com/api/placement-history?mailbox_id=aea15df0-fdf2-4c63-af5e-589f2a410e4e" \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "eeb975ef-ec62-4fac-815a-f9ccd484fc49",
"subject": "Your checkout speed",
"created_at": "2026-09-14T18:10:51.816Z",
"mailbox_id": "aea15df0-fdf2-4c63-af5e-589f2a410e4e",
"mailbox": "rae@nudgeone.test",
"seeds": 3,
"sent": 3,
"failed": 0,
"recorded": 2,
"waiting_for": [
"seed3@outlook.test"
],
"providers": [
{
"provider": "Gmail",
"sent": 2,
"recorded": 2,
"inbox": 1,
"promotions": 0,
"spam": 1,
"missing": 0
},
{
"provider": "Outlook",
"sent": 1,
"recorded": 0,
"inbox": 0,
"promotions": 0,
"spam": 0,
"missing": 0
}
]
},
{
"id": "fd3e9b19-8303-47d8-8120-0c558d790e72",
"subject": "Your checkout speed",
"created_at": "2026-09-07T18:10:51.809Z",
"mailbox_id": "aea15df0-fdf2-4c63-af5e-589f2a410e4e",
"mailbox": "rae@nudgeone.test",
"seeds": 3,
"sent": 3,
"failed": 0,
"recorded": 3,
"waiting_for": [],
"providers": [
{
"provider": "Gmail",
"sent": 2,
"recorded": 2,
"inbox": 2,
"promotions": 0,
"spam": 0,
"missing": 0
},
{
"provider": "Outlook",
"sent": 1,
"recorded": 1,
"inbox": 1,
"promotions": 0,
"spam": 0,
"missing": 0
}
]
}
]PUT /placement-schedule
Sends subject and body from every active inbox to every seed address, once every every_days days: 7 by default and never fewer. An inbox without room under its cap or its domain’s is skipped that time, and last_note says so. A new schedule’s first run is due at once. Changing one keeps its next run. When a provider’s results for a test are all recorded and it landed in the inbox less than on that inbox’s test before, the workspace’s reply notifications say so once.
Request
curl -X PUT https://craftnudge.com/api/placement-schedule \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Your checkout speed",
"body": "Hi, a quick one about your checkout.",
"every_days": 14
}'Response, 200
{
"subject": "Your checkout speed",
"body": "Hi, a quick one about your checkout.",
"every_days": 14,
"next_run_at": "2026-09-21T18:10:51.818Z",
"last_run_at": "2026-09-14T18:10:51.818Z",
"last_note": "Sent from rae@nudgeone.test to 3 seed addresses. Skipped sam@nudgetwo.test: sam@nudgetwo.test is at its daily cap: 1 left today, 3 seed addresses."
}GET /placement-schedule
The schedule with its next run, its last run and what that run sent and skipped. null when there is none.
Request
curl https://craftnudge.com/api/placement-schedule \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"subject": "Your checkout speed",
"body": "Hi, a quick one about your checkout.",
"every_days": 7,
"next_run_at": "2026-09-21T18:10:51.818Z",
"last_run_at": "2026-09-14T18:10:51.818Z",
"last_note": "Sent from rae@nudgeone.test to 3 seed addresses. Skipped sam@nudgetwo.test: sam@nudgetwo.test is at its daily cap: 1 left today, 3 seed addresses."
}DELETE /placement-schedule
No more scheduled tests. Past tests and their results stay.
Request
curl -X DELETE https://craftnudge.com/api/placement-schedule \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}Help from a model on the workspace’s own key, used only when asked. Nothing here sends or saves. An owner sets the key and a monthly spending cap in the app. Without a key, or with the cap reached, a call answers 400 with a reason.
GET /assisted/settings
Whether a key is saved, the monthly cap, and what is spent this month, in millionths of a dollar.
Request
curl https://craftnudge.com/api/assisted/settings \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"has_key": true,
"monthly_cap_micros": 5000000,
"spent_this_month_micros": 0
}POST /assisted/replies/:id/draft
A draft answer to one reply, to read and change before sending it with Answer a reply.
Request
curl -X POST https://craftnudge.com/api/assisted/replies/5c4773ad-b16a-4a91-aee9-f10264c620d0/draft \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"draft": "Hi Rosa,\n\nHappy to explain. Agencies that refer a shop to us get 10% of what that shop pays us for the first year, paid every month. There is nothing to sign up for until the first shop is live.\n\nWould a short call on Thursday help?\n\nJordan"
}POST /assisted/replies/:id/suggestions
Up to three short answers to one reply, each a different way to answer, drawing on up to ten of the team’s saved replies.
Request
curl -X POST https://craftnudge.com/api/assisted/replies/5c4773ad-b16a-4a91-aee9-f10264c620d0/suggestions \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"suggestions": [
{
"intent": "Explain the referral",
"text": "Hi Rosa,\n\nAgencies that refer a shop get 10% of what that shop pays us for the first year, paid monthly. Happy to walk you through it.\n\nJordan"
},
{
"intent": "Book a call",
"text": "Hi Rosa,\n\nGreat to hear. Would a 15 minute call this week work? Thursday afternoon is open on my side.\n\nJordan"
}
]
}POST /assisted/subjects
Up to three subject lines for a body you wrote. A line may use only merge fields the body already uses.
Request
curl -X POST https://craftnudge.com/api/assisted/subjects \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "Hi {{first_name|there}},\n\nHow are you handling returns at {{company}}?\n\nJordan\n\nNot relevant? {{unsubscribe}}"
}'Response, 200
{
"subjects": [
"Returns at {{company}}",
"What a return costs {{company}} today",
"A question about your returns"
]
}POST /assisted/rewrite
style is shorter or plainer. Every merge field in the body is kept, and none is added.
Request
curl -X POST https://craftnudge.com/api/assisted/rewrite \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"style": "shorter",
"body": "Hi {{first_name|there}},\n\nI saw {{company}} has been growing fast this year, and I wanted to ask how you are handling returns now that volume is up. We work with shops your size and usually cut the cost per return by about a third.\n\nWould a short call be worth it?\n\nJordan\n\nNot relevant? {{unsubscribe}}"
}'Response, 200
{
"body": "Hi {{first_name|there}},\n\nHow is {{company}} handling returns? We cut the cost per return by about a third for shops your size.\n\nWorth a short call?\n\nJordan\n\nNot relevant? {{unsubscribe}}"
}The workspace’s settings and where notifications go.
GET /workspace
Its name, timezone, re-contact window, unsubscribe wording, default from name and tracking domain, and the agency name, logo key and dashboard domain it is branded with.
Request
curl https://craftnudge.com/api/workspace \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"name": "Acme Outreach",
"timezone": "Europe/London",
"unsub_text": null,
"unsub_why": null,
"default_from_name": null,
"assisted_facts": null,
"recontact_days": 180,
"system_mailbox_id": null,
"tracking_domain": "links.acme.example",
"tracking_domain_ok": true,
"tracking_cname_target": "app.craftnudge.example",
"delete_at": null,
"brand_name": null,
"brand_logo_key": null,
"dashboard_domain": null
}GET /workspace/activity
The latest 200 changes in the workspace, newest first: campaigns started and paused, settings, inboxes and their caps, team members, client report links and a request to delete the workspace. Each has its details, who made it and when. A change made with an API key has no person, so by is null.
Request
curl https://craftnudge.com/api/workspace/activity \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"entries": [
{
"action": "inbox.removed",
"detail": {
"email": "hello@tallpine.example"
},
"by": null,
"at": "2026-09-14T22:17:33.686Z"
},
{
"action": "inbox.added",
"detail": {
"email": "hello@tallpine.example",
"daily_cap": 20
},
"by": null,
"at": "2026-09-14T22:17:33.644Z"
}
]
}GET /workspace/onboarding
The first steps a new workspace goes through, each done or not, and whether all of them are.
Request
curl https://craftnudge.com/api/workspace/onboarding \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"steps": [
{
"key": "inbox",
"label": "Connect an inbox",
"href": "/accounts",
"done": true
},
{
"key": "tracking_domain",
"label": "Set up a tracking domain",
"href": "/settings/workspace",
"done": true
}
],
"complete": true
}GET /settings
Whether leads are deduped across campaigns, the weekly ramp schedule, and the bounce rate that holds a ramp back.
Request
curl https://craftnudge.com/api/settings \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"dedupe_across_campaigns": true,
"ramp_schedule": [
5,
10,
12,
15,
20,
25
],
"ramp_bounce_limit": 5
}PATCH /settings
Any of dedupe_across_campaigns, ramp_schedule and ramp_bounce_limit. Any other field is refused.
Request
curl -X PATCH https://craftnudge.com/api/settings \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ramp_bounce_limit": 4
}'Response, 200
{
"dedupe_across_campaigns": true,
"ramp_schedule": [
5,
10,
12,
15,
20,
25
],
"ramp_bounce_limit": 4
}GET /verification/settings
The verifier the workspace checks addresses with, whether a key is saved, and how many addresses were checked this month. The key is never returned. An owner saves it in the app.
Request
curl https://craftnudge.com/api/verification/settings \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"provider": "reoon",
"has_key": true,
"checks_this_month": 9
}GET /notifications
Email, Telegram and the webhook, each on or off, the webhook’s address and events, and how many people have linked Telegram.
Request
curl https://craftnudge.com/api/notifications \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"notify_email": "jordan@acme.example",
"notify_email_on": true,
"telegram_on": true,
"telegram_available": true,
"telegram_linked_as": null,
"telegram_linked_count": 0,
"webhook_url": "https://hooks.acme.example/craftnudge",
"webhook_on": true,
"webhook_events": [
"reply",
"interested",
"bounce"
]
}PATCH /notifications
Any of notify_email, notify_email_on, telegram_on, webhook_url, webhook_on and webhook_events. A webhook address must be on the public internet. Telegram is linked by a person, in the app.
Request
curl -X PATCH https://craftnudge.com/api/notifications \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"notify_email_on": true,
"webhook_events": [
"reply",
"interested",
"bounce"
]
}'Response, 200
{
"notify_email": "jordan@acme.example",
"notify_email_on": true,
"telegram_on": true,
"telegram_available": true,
"telegram_linked_as": null,
"telegram_linked_count": 0,
"webhook_url": "https://hooks.acme.example/craftnudge",
"webhook_on": true,
"webhook_events": [
"reply",
"interested",
"bounce"
]
}Webhook events has what each event sends and how to check a delivery.
GET /integrations/webhooks/events
Every event, when it fires, and an example body.
Request
curl https://craftnudge.com/api/integrations/webhooks/events \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"event": "sent",
"when": "An email left one of your inboxes. Announced within a minute of going out, once per email.",
"example": {
"event": "sent",
"email": "owner@shop.example",
"campaign": "Woo speed UK",
"mailbox": "rae@yourdomain.example",
"step": "1",
"subject": "Your checkout on Shop Example",
"sent_at": "2026-09-13T09:14:02.000Z"
}
},
{
"event": "reply",
"when": "A human answered. Out-of-office and bounces are not replies.",
"example": {
"event": "reply",
"domain": "shop.example",
"person": "Rebecca",
"text": "Sounds good, call me Thursday?",
"campaign": "Woo speed UK",
"mailbox": "rae@yourdomain.example",
"reply_id": "0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b",
"link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}
},
{
"event": "bounce",
"when": "The address rejected the email.",
"example": {
"event": "bounce",
"email": "nobody@shop.example",
"campaign": "Woo speed UK",
"mailbox": "rae@yourdomain.example",
"code": "5.1.1",
"category": "bad_address",
"link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}
},
{
"event": "unsubscribe",
"when": "Someone used the unsubscribe link. They are on your blocklist from this moment.",
"example": {
"event": "unsubscribe",
"email": "owner@shop.example",
"campaign": "Woo speed UK"
}
},
{
"event": "interested",
"when": "You marked a reply as interested. Sent each time it is marked, including again.",
"example": {
"event": "interested",
"email": "owner@shop.example",
"link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}
},
{
"event": "interest_changed",
"when": "You moved a reply to a different status. Not sent when the status did not change.",
"example": {
"event": "interest_changed",
"email": "owner@shop.example",
"from": "lead",
"to": "meeting_booked",
"link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}
},
{
"event": "task_due",
"when": "A lead reached a manual step, so a task is waiting for someone. Sent once per task.",
"example": {
"event": "task_due",
"email": "owner@shop.example",
"campaign": "Woo speed UK",
"kind": "linkedin",
"text": "Connect on LinkedIn and mention the checkout speed."
}
}
]POST /hooks/subscribe
For Zapier and tools like it. The URL gets the same signed, retried deliveries as the workspace webhook. Subscribing the same URL twice returns the first subscription.
Request
curl -X POST https://craftnudge.com/api/hooks/subscribe \
-H "Authorization: Bearer $CRAFTNUDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "reply",
"target_url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/"
}'Response, 201
{
"id": "8370d89c-0dc4-4a8b-b84a-0b408550954c",
"event": "reply",
"target_url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/"
}GET /hooks/sample/:event
The documented example for an event, never real data.
Request
curl https://craftnudge.com/api/hooks/sample/reply \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"event": "reply",
"domain": "shop.example",
"person": "Rebecca",
"text": "Sounds good, call me Thursday?",
"campaign": "Woo speed UK",
"mailbox": "rae@yourdomain.example",
"reply_id": "0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b",
"link": "https://app.craftnudge.example/unibox?reply=0b5e8f7a-2c1d-4e3f-9a8b-7c6d5e4f3a2b"
}
]DELETE /hooks/subscribe/:id
Ends a subscription. A URL that answers 410 ends its own.
Request
curl -X DELETE https://craftnudge.com/api/hooks/subscribe/8370d89c-0dc4-4a8b-b84a-0b408550954c \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
{
"deleted": true
}GET /integrations/webhooks/deliveries
Deliveries to your webhook and REST hooks, with each one’s status, attempts, last answer or error, and when the next try is due.
Request
curl https://craftnudge.com/api/integrations/webhooks/deliveries \ -H "Authorization: Bearer $CRAFTNUDGE_API_KEY"
Response, 200
[
{
"id": "6d7167e4-9f0c-4763-95c0-c3daeb23830c",
"event": "interested",
"status": "pending",
"attempts": 1,
"last_status": null,
"last_error": "that webhook address is not on the public internet",
"next_attempt_at": "2026-09-14T22:18:33.339Z",
"delivered_at": null,
"created_at": "2026-09-14T22:17:33.332Z",
"to": "hooks.acme.example"
}
]