GET /v1/newsletters
List your newsletters
Any key
curl -X GET https://app.noosletter.com/v1/newsletters \ -H "Authorization: Bearer $NOOS_API_KEY"
Add subscribers from your own systems, write and send emails, and hear about changes as they happen. Everything is scoped to one workspace and follows the same rules as the app. There's no per-person open, click or view data anywhere in the API.
Create a key in Settings → Developers. A read key can only read; a read and write key can also change things and send. Keep keys on your server. Send the key as a bearer token:
curl https://app.noosletter.com/v1/newsletters \ -H "Authorization: Bearer $NOOS_API_KEY"
{ "data": … }, or { "error": { "type", "message" } }.limit (up to 100) and the next_cursor you were given.429 with a Retry-After header.Idempotency-Key header. Repeat it within 24 hours and you get the first response back instead of a second subscriber or a second send.| 400 invalid_request | Something in the request isn't right. The message says what. |
| 401 unauthorized | The key is missing, wrong or revoked. |
| 403 forbidden | A read key tried to change something. |
| 404 not_found | It doesn't exist in this workspace. |
| 409 | It conflicts with how things are, e.g. already_sent, already_exists, unsubscribed_by_reader. |
| 422 | It can't be done as asked, e.g. invalid_email, blocked, not_ready, no_recipients. |
| 429 rate_limited | Slow down; see Retry-After. |
GET /v1/subscribers
List subscribers
Newest first.
Any key
limit 1 to 100, default 25cursor The next_cursor from the previous pageemail Find one addressnewsletter_id Only people subscribed to this newsletterstatus With newsletter_id: subscribed (default) or unsubscribedcurl -X GET https://app.noosletter.com/v1/subscribers \ -H "Authorization: Bearer $NOOS_API_KEY"
POST /v1/subscribers
Add a subscriber
Runs your Blocklist and address verification; an address that can't receive email is refused. To subscribe them to newsletters, say how they agreed in consent.basis: it's kept with the subscription. Adding someone without newsletters creates a contact who gets nothing until they're subscribed.
Read and write key · accepts Idempotency-Key
email string, requiredfirst_name stringlast_name stringnewsletter_ids array of string. Subscribe them to these newsletters. Needs consent.consent object. Contains consent.basis.tag_ids array of stringcurl -X POST https://app.noosletter.com/v1/subscribers \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","first_name":"Ada","newsletter_ids":["NEWSLETTER_ID"],"consent":{"basis":"Ticked the box at checkout"}}'GET /v1/subscribers/{id}
Get a subscriber
Any key
curl -X GET https://app.noosletter.com/v1/subscribers/ID \ -H "Authorization: Bearer $NOOS_API_KEY"
PATCH /v1/subscribers/{id}
Update a subscriber's name
Read and write key
first_name objectlast_name objectcurl -X PATCH https://app.noosletter.com/v1/subscribers/ID \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"Ada"}'POST /v1/subscribers/{id}/subscriptions
Subscribe to a newsletter
Needs a consent basis. Someone who unsubscribed themselves can only come back themselves, so that's refused with unsubscribed_by_reader.
Read and write key · accepts Idempotency-Key
newsletter_id string, requiredconsent object, required. Contains consent.basis.curl -X POST https://app.noosletter.com/v1/subscribers/ID/subscriptions \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"newsletter_id":"NEWSLETTER_ID","consent":{"basis":"Asked at our event"}}'DELETE /v1/subscribers/{id}/subscriptions/{newsletter_id}
Unsubscribe from a newsletter
Read and write key
curl -X DELETE https://app.noosletter.com/v1/subscribers/ID/subscriptions/NEWSLETTER_ID \ -H "Authorization: Bearer $NOOS_API_KEY"
POST /v1/subscribers/{id}/tags
Tag a subscriber
Read and write key
tag_id string, requiredcurl -X POST https://app.noosletter.com/v1/subscribers/ID/tags \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tag_id":"TAG_ID"}'DELETE /v1/subscribers/{id}/tags/{tag_id}
Remove a tag
Read and write key
curl -X DELETE https://app.noosletter.com/v1/subscribers/ID/tags/TAG_ID \ -H "Authorization: Bearer $NOOS_API_KEY"
GET /v1/segments
List segments
Any key
curl -X GET https://app.noosletter.com/v1/segments \ -H "Authorization: Bearer $NOOS_API_KEY"
GET /v1/segments/{id}
Get a segment and how many people are in it
Any key
curl -X GET https://app.noosletter.com/v1/segments/ID \ -H "Authorization: Bearer $NOOS_API_KEY"
GET /v1/emails
List emails
Any key
limit 1 to 100, default 25cursor The next_cursor from the previous pagestatus draft, scheduled, sending, sent, failed or cancelledcurl -X GET https://app.noosletter.com/v1/emails \ -H "Authorization: Bearer $NOOS_API_KEY"
POST /v1/emails
Create a draft
Written in Markdown, the same as the editor. It's sent from your default verified sender.
Read and write key · accepts Idempotency-Key
newsletter_id string. Needed when the account has more than one newslettername string. Defaults to the subjectsubject stringpreview_text objectbody_markdown string. The email, in Markdowncurl -X POST https://app.noosletter.com/v1/emails \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"subject":"This week","body_markdown":"# Hello\n\nIt's been a busy week."}'GET /v1/emails/{id}
Get an email
Any key
curl -X GET https://app.noosletter.com/v1/emails/ID \ -H "Authorization: Bearer $NOOS_API_KEY"
PATCH /v1/emails/{id}
Edit a draft
Read and write key
name stringnewsletter_id stringsubject stringpreview_text objectbody_markdown stringcurl -X PATCH https://app.noosletter.com/v1/emails/ID \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject":"This week, properly"}'POST /v1/emails/{id}/schedule
Schedule
Checks there's a subject, a body, a verified sender and your postal address first.
Read and write key · accepts Idempotency-Key
send_at string, required. When to send, ISO 8601 with a time zone offsettimezone string. IANA time zone to show it in, e.g. Europe/London. Defaults to the account's.curl -X POST https://app.noosletter.com/v1/emails/ID/schedule \
-H "Authorization: Bearer $NOOS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"send_at":"2026-10-02T08:00:00+01:00"}'POST /v1/emails/{id}/cancel
Cancel a schedule
It goes back to being a draft.
Read and write key
curl -X POST https://app.noosletter.com/v1/emails/ID/cancel \ -H "Authorization: Bearer $NOOS_API_KEY"
POST /v1/emails/{id}/send
Send now
Sends to everyone subscribed to the email's newsletter who can receive it. Pass an Idempotency-Key so a retry can never send twice; without one, a second send is refused.
Read and write key · accepts Idempotency-Key
curl -X POST https://app.noosletter.com/v1/emails/ID/send \ -H "Authorization: Bearer $NOOS_API_KEY" \ -H "Idempotency-Key: $(uuidgen)"
GET /v1/emails/{id}/stats
Stats
Anonymous totals. Never who opened or clicked.
Any key
curl -X GET https://app.noosletter.com/v1/emails/ID/stats \ -H "Authorization: Bearer $NOOS_API_KEY"
Add an https endpoint in Settings → Developers and choose its events. We POST a JSON body like this, signed with the endpoint's secret:
{
"id": "delivery id",
"type": "subscriber.created",
"created_at": "2026-09-26T09:14:03.120Z",
"data": {
"subscriber": { "id": "…", "email": "ada@example.com", "first_name": "Ada", "last_name": null },
"newsletter": { "id": "…", "name": "The Friday Letter" },
"source": "hosted_form",
"occurred_at": "2026-09-26T09:14:03.100Z"
}
}Events:
subscriber.createdsubscriber.resubscribedsubscriber.unsubscribedemail.scheduledemail.sentThere are no open or click events, and no payload says who opened or clicked. Answer with any 2xx within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, then marked failed; you can retry it from the delivery log. Redirects aren't followed.
Check each request came from us with the Noosletter-Signature header (t= time, v1= HMAC-SHA256 of <t>.<raw body>):
import crypto from "node:crypto";
// header: the Noosletter-Signature header, e.g. "t=1760000000,v1=5f2c…"
// body: the raw request body, exactly as received
export function isFromNoosletter(secret, body, header) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}noos wraps the API for scripts and quick jobs. It has no logic of its own.
npm install -g @noosletter/cli export NOOS_API_KEY=noos_… noos newsletters noos subscribers add ada@example.com --newsletter NEWSLETTER_ID --consent "Asked at our event" noos emails create --subject "This week" --file issue.md noos emails send EMAIL_ID --yes noos stats EMAIL_ID