Skip to content

Newsletter API: how to send a broadcast from your code, from consent to the report

Published Updated 10 min readBy the Rasket team

A fan of envelopes spreading out from one point, each carrying a small tag, drawn as white and violet outlines on black.

What a newsletter API is

A newsletter API is an HTTP interface for sending one message to many people who asked for it: the audience, the consent, the message and the report are all objects you create and read from code rather than from a campaign editor. The same thing is sometimes called a broadcast API, because the send itself is called a broadcast.

What separates it from the transactional send you already have is not the transport. Each recipient of a broadcast is one ordinary email through the same pipeline, with its own delivery events. The difference is what has to be true before the send is allowed: the person opted in, they can opt out in one action, and the message says who you are and where you are. A newsletter API is a transactional API with those rules built in, and the transactional-versus-marketing post explains why the two kinds of mail should be kept apart even when one product sends both.

If the question you arrived with was how to “send newsletter programmatically”, the reason is probably the same as for any other part of your product: the audience already lives in your database, the trigger is already an event you own, and a monthly job that assembles a digest and posts it is more reliable than a person remembering to press a button.

Contacts, topics and segments

Three objects, and it pays to be precise about which is which, because the consent lives on one of them and the audience on another.

The three objects a broadcast is built from
ContactTopicSegment
What it isAn address, optional names, and typed properties you declaredA subscription category with a default_subscriptionA saved filter over contacts, or a hand-built list, or both
What it recordsWho the person isWhat each person agreed to hear about, as a consent ledgerWhich people a broadcast goes to
When it changesOn every write; an existing address is updated, never duplicatedNever: the default is fixed at creationNever: the filter is immutable, membership is evaluated at send time
Shown to the readerNoYes, on the hosted preference pageNo
Best forHolding the properties a merge variable printsLetting someone leave the digest without leaving the security bulletinsNaming the audience of one broadcast so its report keeps meaning the same people

The rule that follows from the last row is the one people trip on: a segment’s filter never changes. Rename it freely; for a different audience, create a new one. That is what lets a report on a broadcast you sent last month keep naming the audience it actually went to. The segments reference lists the fields a filter can test and the operators each accepts.

Sending a newsletter, step by step

Every call reads the key from RASKET_API_KEY and sets a User-Agent, because a request without one is refused. Ids from earlier steps are held in shell variables.

  1. Create a topicPOST /topics with a name, a description for the preference page and a default_subscription. For a newsletter people sign up to, the default is opt_out, and it cannot be changed after creation.
  2. Add contacts with their consentPOST /contacts with the address, any properties, and topics: [{ id, subscription: "opt_in" }]. The topic entry writes a consent record with its source and time; an address you already hold is updated rather than duplicated.
  3. Create a segment for the audiencePOST /segments with a filter whose condition is { topic, subscription: "opt_in" }. Membership is evaluated when the segment is read and when a broadcast sends, never stored, and the filter itself never changes.
  4. Create the broadcastPOST /broadcasts with the segment_id, a from address on a verified domain, a subject, and either html carrying {{{FIRST_NAME|there}}} and {{{UNSUBSCRIBE_URL}}} or template: { id } naming a published template.
  5. Check the gate, then sendGET /broadcasts/{id}/checklist shows every condition of the send gate, passed or not. When all pass, POST /broadcasts/{id}/send with an Idempotency-Key, now or with a scheduled_at up to thirty days out.
  6. Read the reportGET /broadcasts/{id}/recipients?type= for who was sent, delivered, opened, clicked, bounced, complained, unsubscribed or suppressed, and /clicked-links for every URL with its total and unique clicks.

1. Create a topic

curl https://api.rasket.com/topics \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{    "name": "Product updates",    "description": "What shipped, once a month.",    "default_subscription": "opt_out",    "visibility": "public"  }'

default_subscription is what a contact with no recorded choice is treated as, and it cannot be changed later. For a newsletter people sign up to, opt_out is the honest default: nobody is in until they said so. public lists it on every contact’s preference page.

2. Add contacts with their consent

curl https://api.rasket.com/contacts \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{    "email": "ronald.williams@example.com",    "first_name": "Ronald",    "topics": [{ "id": "'"$TOPIC_ID"'", "subscription": "opt_in" }]  }'

The topics entry is the consent. Every subscription change writes a record with its source and time, which is what you will point at if anyone asks how this person came to be on the list. An address you already hold is updated and its id returned, so this call is safe to repeat. Where the opt-in came from — a form, and ideally a confirmed one — is your side of the record.

3. Create a segment

curl https://api.rasket.com/segments \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{    "name": "Product updates, opted in",    "filter": {      "all": [{ "topic": "'"$TOPIC_ID"'", "subscription": "opt_in" }]    }  }'

A filter is a tree of all, any and not groups over conditions, and a condition is either a field test or a topic subscription. This one is the smallest useful audience: everyone opted in to the topic. Membership is never stored — it is worked out when you read the segment and again when a broadcast sends — so a contact who opts in tomorrow is in tomorrow’s send.

4. Create the broadcast

curl https://api.rasket.com/broadcasts \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{    "name": "September product update",    "segment_id": "'"$SEGMENT_ID"'",    "topic_id": "'"$TOPIC_ID"'",    "from": "Acme <news@send.acme.example>",    "subject": "What shipped in September",    "html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Three things shipped this month.</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"  }'

The body carries two merge variables. {{{FIRST_NAME|there}}} prints the contact’s first name with a fallback, and {{{UNSUBSCRIBE_URL}}} is the one-click link. Any declared contact property works the same way, as do EMAIL and PREFERENCES_URL. If you would rather keep the content out of the request, pass "template": { "id": "monthly-digest" } naming a published template instead of html. Without send: true this is a draft, which is what you want when somebody is going to read it first.

5. Check the gate, then send

# Read the gate firstcurl https://api.rasket.com/broadcasts/$BROADCAST_ID/checklist \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"
# Then queue it — now, or at a time you choosecurl https://api.rasket.com/broadcasts/$BROADCAST_ID/send \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Idempotency-Key: newsletter-2026-09-send" \  -H "Content-Type: application/json" \  -d '{ "scheduled_at": "2026-09-17T09:00:00Z" }'

The checklist answers with every condition of the send gate, passed or not, in the order a send checks them: a from address on a verified domain, a postal address on file, a segment that resolves, a body. A failed condition carries the same name and message the send would refuse with, so you can fix it without a failed attempt. The send itself takes an optional scheduled_at between one minute and thirty days out; omit it to queue now. A scheduled broadcast can be cancelled with POST /broadcasts/{id}/cancel until it starts.

6. Read the report

curl "https://api.rasket.com/broadcasts/$BROADCAST_ID/recipients?type=clicked&limit=20" \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"
curl https://api.rasket.com/broadcasts/$BROADCAST_ID/clicked-links \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"

type is one of sent, delivered, opened, clicked, bounced, complained, unsubscribed or suppressed, and the last one names the reason each person was left out. clicked-links lists every URL with its total and unique clicks. Because each recipient is an ordinary send, the same facts also arrive as webhook events per email, which is how a complaint reaches your own database the moment it happens.

The compliance the API enforces

Some of the rules of bulk mail are yours to keep. The ones below are kept for you, because a broadcast that breaks them should not be possible to send.

  • A postal address on file. The gate refuses a send until your team has one. In the United States, the FTC’s CAN-SPAM compliance guide lists a valid physical postal address among what every commercial message must carry, alongside honest header information, a non-deceptive subject line, a way to opt out and prompt honouring of the opt-out. This is not legal advice; read the guide for the detail and your own jurisdiction’s rules for the rest.
  • An unsubscribe link in the body. Put {{{UNSUBSCRIBE_URL}}} or {{{PREFERENCES_URL}}} where you want it. A body with neither gets a footer appended carrying an unsubscribe link and the postal address, so no broadcast can leave without one.
  • One-click unsubscribe headers. Every broadcast leaves with the header RFC 2369 defined and the one RFC 8058 added, which together let a mail app show its own unsubscribe control and act on it with a single POST.
  • A consent ledger. Opt-ins written over the API, opt-outs from the headers and the preference page: all of it lands in one record per contact and topic, with a source and a time, and a broadcast scoped to a topic goes only to contacts whose effective subscription is opt_in.
List-Unsubscribe: <https://www.rasket.com/u/v1.1.eyJ0…>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

The headers are the part mailbox providers now check. Google’s sender guidelines require bulk senders to support one-click unsubscribe and to honour it within two days, and the header pair above is how that requirement is met on the wire. The reader’s click is honoured immediately and written to the ledger against the topic, not against their whole relationship with you.

Keep the newsletter off your transactional reputation

Mailbox providers score the domain and address mail comes from, on how recipients treat it. A newsletter draws more complaints than a receipt, because some people forget they subscribed. If both leave from the same domain, the complaints are spent from one balance, and the first symptom is password resets landing in spam for customers who never saw the campaign.

Two separations keep that from happening. The first is Rasket’s: broadcasts are metered separately from transactional volume, so a large send answers marketing_quota_exceeded rather than eating the quota a receipt needs. The second is yours: verify a second sending domain and send the newsletter from it.

One subdomain each

Send receipts and resets from send.acme.example and the newsletter from news.acme.example. They are separate sending domains, separately signed and separately scored, so a bad month for the digest is a bad month for the digest and nothing else. The domains page covers adding the second one; it takes the same records as the first.

One last habit that costs nothing: read the complained and unsubscribed recipients after every send, not just the opens. Those two lists are the audience telling you what they did not want, and a list that shrinks by the people who did not want it is a list whose next send will land. The glossary entry defines the terms this post used in passing, and the broadcasts reference has every field of every call above.

Frequently asked questions

What is the difference between a newsletter API and a transactional email API?

The transport is the same: each recipient of a broadcast is one ordinary email through the same pipeline. The difference is the rules applied before the send: the recipient must have opted in, the message must carry an unsubscribe and a postal address, and the whole thing is metered separately so it cannot exhaust the quota a receipt needs.

Can I send a broadcast from a template instead of inline HTML?

Yes. Pass template: { id } naming a published template in place of html and text, and the broadcast copies that template's published version. Merge variables such as {{{FIRST_NAME|there}}} and {{{UNSUBSCRIBE_URL}}} work the same way in either.

What stops a broadcast from sending?

Four conditions, checked in order: the from address is on a domain verified for sending, the team has a postal address on file, the segment resolves, and the broadcast has a body. A failed condition is a 422 that creates nothing, and GET /broadcasts/{id}/checklist reports the same conditions beforehand.

Do I have to add an unsubscribe link myself?

You should place {{{UNSUBSCRIBE_URL}}} or {{{PREFERENCES_URL}}} where you want it, but a body with neither gets a footer appended with an unsubscribe link and your postal address. Separately, every broadcast carries the RFC 8058 List-Unsubscribe headers, whatever the body says.

Can I change a segment after I have sent to it?

You can rename it, but its filter never changes. A different audience is a new segment, which is what lets the report on a broadcast you sent last month keep naming the people it actually went to. Membership is evaluated at send time, so new opt-ins are picked up automatically.

Why should newsletters go out from a different subdomain?

Mailbox providers score the domain mail comes from on how recipients treat it, and a newsletter draws more complaints than a receipt. Sending the digest from news.acme.example and receipts from send.acme.example keeps the two scores apart, so a bad campaign cannot push password resets into spam.

Sources

  1. RFC 8058: Signaling One-Click Functionality for List Email HeadersRFC Editor, read 2026-09-16
  2. RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List CommandsRFC Editor, read 2026-09-16
  3. CAN-SPAM Act: A Compliance Guide for BusinessFederal Trade Commission, read 2026-09-16
  • BroadcastsAn audience you own: contacts with typed properties, segments, topics people subscribe to, and broadcasts sent through the same pipeline as your other mail.
  • BroadcastsOne message to a segment: the draft, the gate, the send and the results.
  • SegmentsAudiences defined by a filter, by hand, or both.
  • BroadcastA broadcast is one message sent to many people at once: a newsletter, a product announcement, a release note. What separates it from transactional mail is who…
  • Transactional vs marketing email: how to send eachWhat separates transactional email from marketing email — the trigger, consent, unsubscribe headers and reputation — and how to send both from one API.

Start sending this morning

Sign up, verify a domain and send your first email in minutes.