Docs

API reference.

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.

Base URL and keys

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.

Requests and errors

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.

Contents

Inboxes

The mailboxes you send from. A password is stored encrypted and never comes back.

GET /mailboxes

List inboxes

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

Connect an inbox

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

Pause an inbox

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

Resume an inbox

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

Change an inbox

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

Change several inboxes

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

Inbox 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

Check 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

Delete an inbox

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
}

Campaigns

A campaign is a draft until it is launched. Send only the fields you want to change.

POST /campaigns

Make a campaign

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

List 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

Change a campaign

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

Set the 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

Save a sequence as a template

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

List 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

Use a 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

Delete a template

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

Read a campaign

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

Add leads as JSON

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

Add leads from a CSV

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

Add leads from a Google 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

List a campaign’s 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

Save a leads filter

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

List saved 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

Delete a saved filter

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

Export leads as 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

Preview steps

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

Check merge fields

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

Check the copy

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

Check mail servers

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

Check 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

Address check results

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

Launch a campaign

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

Pause a campaign

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

Campaign numbers

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

Duplicate a campaign

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

Delete a campaign

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
}

Lead lists

A list keeps people outside any campaign, so the same set can start more than one.

POST /lead-lists

Make a list

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

Fill a list from a CSV

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

List the 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

Start a campaign from a 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

Delete a list

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
}

Leads

One person in one campaign.

GET /leads

Find 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

Read a lead

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

Change a lead

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

A lead’s history

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

A lead for one address

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

Send one email to a lead

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"
}

DELETE /leads/:id

Delete a lead

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
}

Replies

What the unibox shows: replies, bounces and auto-replies, each matched to the send it answers.

GET /replies

List 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

Unread 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

Mark a reply

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

Answer a 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

Assign a reply

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

Unsubscribe from a reply

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

Snooze a reply

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"
}

Saved replies

Answers written once and filled in for a thread. Sending one is an ordinary answer to a reply.

GET /saved-replies

List 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

Example replies

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

Save a reply

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

Change a saved reply

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

Fill in a saved reply

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

Delete a saved reply

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
}

Tasks

Manual steps waiting on someone, such as a LinkedIn connection or a call. The sequence waits until the task is closed.

GET /tasks

List open 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

Mark a task 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

Skip a task

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"
}

Pipeline

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

Read the 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

Move a lead

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"
}

Blocklist

Addresses and whole domains that are never contacted. Checked on every import and again at the moment of sending.

POST /blocklist

Block addresses and domains

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

Import a blocklist

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

Read the 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

Unblock

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
}

Reports

The numbers behind the Analytics and Deliverability pages.

GET /reports/summary

Workspace 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

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

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

Analytics as 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

Sending by domain

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

When people reply

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

DMARC reports

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

Set the DMARC report address

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"
}

Domains and placement tests

The weekly warmup ramp for each sending domain, and placement tests to seed addresses you own.

GET /domains

List sending 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

Start a domain’s ramp

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

Take a domain off its ramp

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

List seed addresses

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

Add a seed address

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

Remove a seed address

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

Run a placement test

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

Record where it landed

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

List 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

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

Schedule placement tests

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

Read the 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

Stop the 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
}

Assisted

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

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

Draft an answer

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

Suggest answers

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

Subject ideas

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

Rewrite a body

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}}"
}

Workspace

The workspace’s settings and where notifications go.

GET /workspace

Read the 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

Recent changes

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

Setup checklist

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

Lead and ramp 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

Change lead and ramp 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

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

Notification settings

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

Change 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"
  ]
}

Webhooks and REST hooks

Webhook events has what each event sends and how to check a delivery.

GET /integrations/webhooks/events

The 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

Subscribe a URL to an event

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

A 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

Unsubscribe

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

Recent 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"
  }
]