Help
Use the API.
Add subscribers, draft campaigns, send one off emails and hear about new sign ups straight away, all from your own code.
1Before you start
The API lives on your own install, at your own web address. Everything here talks to your Newsletter Sender, not to us. In the examples, swap news.yourdomain.com for your install's address.
- Open Settings, then API. In your Newsletter Sender, click Settings then API. Your API address is at the top, with a copy button. It looks like
https://news.yourdomain.com/api/v1. - Make a key. Click Make a key and give it a name you'll recognise later, such as
Website sign up form. Each key belongs to one brand. - Copy the key straight away. It starts
ns_live_and is shown once only. Keep it somewhere safe, such as your password manager or your server's secrets. You can have up to 10 keys for each brand, and you can delete one at any time.
2Keys, limits and errors
Send your key in a header with every request. Either of these works:
Authorization: Bearer ns_live_... X-API-Key: ns_live_...
A key in the web address, such as ?key=ns_live_..., is refused. Web addresses end up in logs and browser history, so if you've ever sent a key that way, delete it and make a new one.
| What | How it works |
|---|---|
| Format | JSON in and JSON out. Send a JSON body with POST and PATCH. |
| Limit | 60 requests a minute for each key. Over that you get 429 and a Retry-After header saying how many seconds to wait. |
| Where from | Call it from your server or a script. Browsers are not allowed to call it directly, which keeps your key out of web pages. |
| Errors | Every error is a plain English message and a link back to this page, such as { "error": "Please check the email address.", "docs": "https://newslettersender.com/help/api/" } |
To check your key works, ask which brand it belongs to:
curl https://news.yourdomain.com/api/v1/me \ -H "Authorization: Bearer ns_live_..."
{ "brand": { "id": "...", "name": "Weekly Notes" }, "docs": "https://newslettersender.com/help/api/" }3Add a subscriber
This is the one most people need: someone fills in a form on your website and you add them to a list.
curl https://news.yourdomain.com/api/v1/subscribers \
-H "Authorization: Bearer ns_live_..." \
-H "Content-Type: application/json" \
-d '{"email":"sam@example.com","name":"Sam","list":"weekly","tags":["spring-offer"]}'
{
"id": "3f9c2a7b81d04e6a",
"email": "sam@example.com",
"name": "Sam",
"fields": {},
"tags": ["spring-offer"],
"status": "pending_confirmation",
"lists": [
{ "id": "...", "slug": "weekly", "name": "Weekly", "status": "pending_confirmation",
"joined_at": "2026-11-06T09:30:00.000Z", "confirmed_at": null, "left_at": null }
],
"created_at": "2026-11-06T09:30:00.000Z",
"result": "pending_confirmation"
}
| Field | What to send |
|---|---|
email | Their email address. Required. |
list or lists | The list's slug or id, such as "weekly", or several as ["weekly", "offers"]. Required. |
name | Their name. Optional. |
fields | Your own extra details, such as { "city": "Leeds" }. Optional. |
tags | Tags to add, such as ["spring-offer"]. Optional. |
confirmed | Leave this out and they get your confirmation email first, which is the safe choice. Send true only for people who have already said yes to your emails somewhere else. |
The result tells you what happened:
| Result | What it means |
|---|---|
pending_confirmation | New, and we've sent them the confirmation email. You get 201. |
active | New and subscribed straight away, because you sent "confirmed": true. You get 201. |
already | They were already on that list, so nothing changed. You get 200. |
elsewhere | They're on another of your lists and this brand allows one list each, so they weren't added. |
4Find, change and remove subscribers
Wherever you see {id or email}, you can use the subscriber's id or their email address.
| Request | What it does |
|---|---|
GET /api/v1/subscribers | A page of subscribers, 50 at a time. Add ?list=weekly, &status=active (or pending_confirmation or unsubscribed) and &limit=100 to narrow it down. When there are more, the reply has a next_cursor: send it back as &cursor=... for the next page. |
GET /api/v1/subscribers/{id or email} | One subscriber, with their fields, tags and every list they've joined or left. |
PATCH /api/v1/subscribers/{id or email} | Change their name, fields or tags. In fields, set a field to null to remove it. tags replaces all their tags; add_tags and remove_tags change just the ones you name. |
POST /api/v1/subscribers/{id or email}/unsubscribe | Unsubscribes them from every list, or just one if you send { "list": "weekly" }. |
DELETE /api/v1/subscribers/{id or email} | Removes everything held about them, for when someone asks to be forgotten. This can't be undone. |
curl -X PATCH https://news.yourdomain.com/api/v1/subscribers/sam@example.com \
-H "Authorization: Bearer ns_live_..." \
-H "Content-Type: application/json" \
-d '{"fields":{"city":"Leeds"},"add_tags":["customer"]}'5Lists
GET /api/v1/lists gives you every list in the brand, with its id, slug, name, description, whether it uses double opt in and how many active subscribers it has. Use the slug or id wherever a request asks for a list.
6Campaigns
The API can draft a campaign, but it never sends one. You check it and press send in the app, so nothing goes out to your whole list by mistake.
curl https://news.yourdomain.com/api/v1/campaigns \
-H "Authorization: Bearer ns_live_..." \
-H "Content-Type: application/json" \
-d '{"subject":"Spring news","list":"weekly","html":"<p>Hello</p>"}'
{ "id": "...", "status": "draft", "edit_url": "https://news.yourdomain.com/#email/...", "audience": 1240 }
| Request | What it does |
|---|---|
POST /api/v1/campaigns | Makes a draft. Send subject and list or lists, and optionally name, preheader, html and tags. The edit_url opens it in the app and audience is how many people it would go to. |
GET /api/v1/campaigns | Your last 100 campaigns, with their status and how many have been sent. |
GET /api/v1/campaigns/{id} | One campaign with its numbers: sent, failed, queued, unsubscribed, bounced and complained. |
7Send a one off email
For a single email to one person, such as a receipt or a password reset. It goes from your brand's From name and address, through the same sending service as your newsletters.
curl https://news.yourdomain.com/api/v1/emails \
-H "Authorization: Bearer ns_live_..." \
-H "Content-Type: application/json" \
-d '{"to":"sam@example.com","subject":"Your receipt","text":"Thanks for your order."}'
{ "sent": true, "message_id": "..." }
Send to, subject and html or text (or both), and optionally reply_to. You get 202 once it's handed to your sending service. Addresses that have bounced or complained are refused, to protect your sending reputation.
8Webhooks
A webhook tells your own system the moment something happens, such as a new subscriber, so you don't have to keep asking.
- Add a webhook. In Settings, API, Webhooks, enter your https address and tick the events you want. You can have up to 5 for each brand.
- Copy the signing secret. It starts
whsec_and is shown once only. You use it to check each message really came from your Newsletter Sender. - Send a test. Click Send a test. Your address gets an event called
test.
| Event | When it's sent |
|---|---|
subscriber.subscribed | Someone is now subscribed (they confirmed, or were added as confirmed). |
subscriber.pending | Someone signed up and has been sent the confirmation email. |
subscriber.unsubscribed | Someone unsubscribed. |
subscriber.bounced | Email to someone bounced, so they won't be sent any more. |
subscriber.complained | Someone marked your email as spam, so they won't be sent any more. |
campaign.started | A campaign started sending. |
campaign.sent | A campaign finished sending. |
campaign.paused | A campaign was paused. |
Each one arrives as a POST with a JSON body like this:
{
"id": "...",
"event": "subscriber.subscribed",
"created_at": "2026-11-06T09:31:00.000Z",
"data": {
"subscriber": { "id": "3f9c2a7b81d04e6a", "email": "sam@example.com", "name": "Sam" },
"campaign_id": null,
"detail": "Weekly",
"at": "2026-11-06T09:31:00.000Z"
}
}
Campaign events have campaign_id, detail and at in data. Three headers come with it: NewsletterSender-Event (the event), NewsletterSender-Delivery (the id) and NewsletterSender-Signature.
9Check the signature
The signature header looks like t=1762421460,v1=5d41.... t is when it was sent, in seconds, and v1 is an HMAC SHA256 of the time, a full stop and the raw body, made with your signing secret. Check it before you trust the message, and turn away anything more than 5 minutes old. In Node:
import crypto from "node:crypto";
// raw is the body exactly as it arrived, before you parse the JSON.
function verify(raw, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const now = Date.now() / 1000;
if (!t || !v1 || Number(t) + 300 < now || Number(t) > now + 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
return want.length === v1.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}
| Delivery | How it works |
|---|---|
| When | Within about a minute of the event. |
| Success | Answer with any 2xx status, such as 200. |
| Retries | Each event is sent once. If your address is down, that event is missed, so you can always fetch the subscriber to catch up. |
| Switched off | After 10 failures in a row the webhook is switched off. Fix your address, then press Turn on under Settings, API, Webhooks. |
10If something goes wrong
Every error comes with a plain English error message saying what to fix. These are the status codes you might see:
| Status | What it means |
|---|---|
400 | Something in the request needs fixing, such as a missing list, a bad email address, JSON that won't read or a key sent in the web address. |
401 | No key, or the key isn't right. Check you copied all of it, and that it hasn't been deleted. |
404 | That list, subscriber, campaign or address doesn't exist. Check the spelling of the slug or id. |
405 | The right address but the wrong method, such as GET where it needs POST. |
409 | We can't do that to this address: it's on your blocklist, it bounced or complained, or sending isn't set up yet. |
429 | Over 60 requests a minute. Wait for the seconds in Retry-After, then try again. |
502 | Your sending service turned the email down. Check it's set up under Settings, and try again. |
Still stuck? Get in touch. Please never send your keys.