Email marketing software for developers: four approaches and the tests that pick one
Published Updated 17 min readBy the Rasket team
Written for developers, AI automation builders and startup founders

Email marketing software for developers is marketing email your code drives: contacts, consent, segments and automations kept current through an API and started by your product’s events. You can get it four ways. If marketers write the campaigns, pick a marketing platform; if engineers own email and the list is your user table, pick one provider for both.
Not writing code? Our comparison of named email marketing platforms is the better read. Marketing email is what people opt in to (newsletters, campaigns, onboarding sequences); transactional email is what something they did triggers (receipts, password resets). Say you are wiring the trial-onboarding emails: a welcome on day one, a setup nudge on day three, “your trial ends Friday” on day twelve. What decides each send lives in your database: did they verify a domain, did they already upgrade. If the email tool learns those facts from a nightly sync, someone who upgraded at 10:02 still gets “your trial ends” at 10:30. Below are the four approaches, the trial tests that tell them apart, those tests scored for each approach and for Rasket (losses included), and how an AI agent can run campaigns safely. Founders can read the first table, the scorecard, the metering section and the last list; the code in between is for your engineers.
Which of the four approaches fits your team?
| Approach | What you build | Strength | Trade-off | Suits |
|---|---|---|---|---|
| Marketing platform with an API added | A sync job that copies users and their attributes into the platform | Editor, A/B tests and reports a marketer can run alone | The list is a copy that lags your database; receipts still need another service | Teams where marketers write and send every campaign |
| Marketing platform plus a transactional email API | Two integrations, two domain setups, two suppression lists kept in step | Each side uses a tool built for it | An unsubscribe or complaint recorded in one tool is unknown to the other until you sync it | Companies with a marketing team and an engineering team that each own a tool |
| Raw sending API plus your own list tooling | Consent records, unsubscribe endpoints, preference pages, segment queries, scheduling, bounce handling | Full control and the lowest price per email | Every piece is yours to build and maintain, and you carry the compliance risk of every bug | High volume or unusual rules that no provider models, with engineers to spare |
| One developer email provider for both | One integration: contacts, events and sends through one API | One domain setup, one suppression list, one log, product data in real time | Marketer tooling is usually thinner. Check whether a big campaign can use up the allowance your password resets need | Product-led teams where engineers own email |
The first two are where most email marketing tools sit, and they are the right answer more often than developers expect. If the person who writes campaigns never opens a terminal, a platform built around their editor serves them better. The cost is the copy: every segment is only as fresh as the last sync, which is exactly the 10:30 email above.
The third is the one engineers are tempted by and should price honestly. A sending API delivers messages; it does not record consent, build the one-click unsubscribe that bulk senders now need, host a preference page or keep one suppression list (the addresses no email may go to because they bounced, complained or unsubscribed) across both kinds of mail. Each is yours to maintain, and most are a legal requirement somewhere. Build your own only for a need no provider meets.
The fourth is what Rasket is. It removes the sync and the second suppression list, and asks you to accept marketer tooling that is newer and narrower.
What should you test on a trial account?
Each check works on any product’s trial account. The first group needs only the dashboard, so whoever chooses the tool can run it; the second needs an engineer with an API key.
Tests anyone can run in the dashboard
- A consent record for every change. Article 7 of the GDPR requires that, where you rely on consent, you can demonstrate it, and that withdrawing is as easy as giving it. A boolean column demonstrates nothing. A topic is a list a contact opts in to or out of, such as “Product news”. Test: opt a contact in to two topics, out of one, and look for how and when each change was recorded.
- Segments evaluated when the campaign sends. Test: save a segment, change a contact so they stop matching, then preview the campaign’s recipients without rebuilding anything. A frozen list still holds them.
- Versioned templates. A template used by a campaign, an automation and a receipt is shared code: edits should wait for a publish. Test: publish a change, restore the previous version, and confirm sends used the published one throughout.
- What marketers will ask for. A/B tests, deliverability reporting beyond rates, and segments built on engagement, such as “clicked nothing in 90 days” for sunsetting inactive subscribers. Developer-first products are usually weakest here. Test: ask for each by name and get a yes or a no.
Tests for an engineer with an API key
- Typed contact properties. A segment that compares
seats > 10quietly misses everyone whose value was imported as the text"12". Test: declare a number property, then send a string for it and see whether the API refuses it. - Automations started by your events, and retries. Test: send the same event twice, as a timeout retry would, and count the runs.
- Signed webhooks for contacts and automations. Test: unsubscribe from a test campaign and time the signed event, then take your endpoint down and replay what it missed.
- An API with a published contract. Test: download the OpenAPI document for the email marketing API (Rasket’s is
https://api.rasket.com/openapi.json) and call one endpoint exactly as it describes. Then check that a campaign can be created, previewed and sent without the dashboard. - An agent door that is not your key. Test: connect an assistant, check that it asked for named permissions you could narrow, and that sending a campaign needed your confirmation.
How do the approaches and Rasket score on those tests?
This page names no products, and a typical product cannot be scored fairly without naming one, so the two middle columns hold only what follows from the approach itself and say to test it where products differ. The platform comparison scores named tools.
| Criterion | Marketing platform | Sending API alone | Rasket today |
|---|---|---|---|
| Typed contact properties | Test it | You build it | Yes, if you declare them; otherwise the first value sets the type |
| Consent record per change | Test it | You build it | Yes. Source and time per topic change, readable over the API |
| Segments evaluated at send | Test it; only as fresh as the last sync | You build it | Yes. Recipients are worked out when the campaign sends |
| Event-triggered automations | Test it; events arrive through its API or a sync | You build it | Yes, but a retried custom event can start a second run |
| Versioned templates | Test it | Test it | Yes. Publish and restore; sends use the published version |
| Signed, replayable webhooks | Test it | Test it | Yes, including contact and automation events |
| Published API contract | Test it | Test it | Yes, an OpenAPI document |
| Agent access without an API key | Test it | Test it | Yes, a remote MCP server with OAuth scopes |
| A/B testing | Usually, yes | You build it | No. There is no split send or winner selection |
| Deliverability reporting | Test it | Test it | Partly. Delivery, bounce, complaint, open and click rates, and per-recipient outcomes; no inbox-placement tests |
| Engagement segments | Usually, yes | You build it | No, but an automation can tag clickers (below) |
| One-click unsubscribe headers on campaigns | Test it | You build it | Yes, on every campaign (RFC 8058) |
| Official client library | Test it | Test it | Not yet (see the last section) |
| One suppression list for campaigns and receipts | No, when receipts go through a second tool: two lists to keep in step | You build it | Yes. A bounce or complaint stops every kind of mail; an unsubscribe stops only marketing |
| Allowance kept back for transactional mail | Not applicable when receipts go through a second tool | Test it | No. Campaigns and receipts share one monthly allowance (metering, below) |
Four Rasket rows need more than a cell. Properties: a contact write that names a new key creates it, typed by that first value, so sending "12" first makes seats a string. Declare the property with POST /contact-properties before the first import, and a wrong-typed value is refused with 422. To fix one that already has the wrong type, PATCH its type: stored values are converted, and the change is refused while a segment or active automation filters on it (contacts reference).
Suppression: one list cuts both ways. A spam complaint about a newsletter also stops that person’s receipts and password resets until you remove the suppression (DELETE /suppressions/{id}); an unsubscribe never does.
Engagement: segments filter on contact fields, properties and topics, not on opens or clicks. The workaround takes three steps in the dashboard. Build an automation triggered on Email clicked whose one step updates the contact, setting a number property such as clicked to 1 (clicks rather than opens, because privacy features such as Apple’s Mail Privacy Protection fetch tracking pixels whether or not anyone reads the message). Then save a segment where properties.clicked exists, and another that wraps the same condition in not for people who never clicked. The property records that someone clicked, not when, so a rolling 90-day window is not possible this way. A scheduled job of yours that sends PATCH /contacts/{id} with { "properties": { "clicked": null } } (a null removes the key) gives a fixed window instead.
Templates: Rasket fills two kinds of variable, and mixing them up is the usual first bug.
| Kind | Written as | Filled from | Where it fills |
|---|---|---|---|
| Send variable | {{first_name}} | The request body of each send, declared on the template | Every send that names the template |
| Merge variable | {{{FIRST_NAME|there}}} | The contact's own fields; text after the bar is the fallback | Campaigns and automation emails only |
Unlike Mustache, where triple braces mean “do not escape”, both kinds are HTML-escaped in the HTML body. A third form, {{brand.company_name}}, reads the brand kit; the template variables post covers all three.
What does triggering a flow from code look like?
One call to POST /events/send. A custom event is a name you declare first with POST /events, such as trial.started, sent for a contact who already exists. A store event needs no declaration and is one of 8 fixed names: store:product.viewed, store:cart.updated, store:checkout.started, store:order.placed, store:order.paid, store:order.fulfilled, store:order.cancelled and store:order.refunded. The Shopify app and the other store integrations send them for you, or your server does. This one records an order for flows to start from; the order confirmation itself is a transactional send through POST /emails:
curl https://api.rasket.com/events/send \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-shop/1.0" \
-H "Content-Type: application/json" \
-d '{
"event": "store:order.placed",
"email": "ronald.williams@example.com",
"idempotency_key": "order-1001-placed",
"payload": { "order_id": "1001", "value": "64.00", "currency": "USD" }
}'Every request needs a User-Agent header naming your app; with none at all the API answers 403. The reply is 202 with { "object": "event", "event": "store:order.placed" } and no event ID, so log your own correlation ID. A store event for an address with no contact is kept and starts nothing; a custom event for an unknown contact is refused with 404. Rasket never creates a contact from an event.
Custom events have no idempotency key yet, and no release date is set
Two identical calls are two events and two runs (events reference). Store events take the idempotency_key above, so a repeated order is recorded once, and POST /emails takes an Idempotency-Key header (idempotency).
Retries: guard the flow itself
A call that timed out may already be stored, so pick one rule per custom event. For a trial welcome, a missed email is cheaper than a duplicate: log the timeout and do not retry, as this Node 18+ call does.
let res;
try {
res = await fetch("https://api.rasket.com/events/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
"User-Agent": "acme-app/1.0",
"Content-Type": "application/json",
},
body: JSON.stringify({ event: "trial.started", email: "ronald.williams@example.com" }),
signal: AbortSignal.timeout(10_000),
});
} catch (error) {
// Anything but a timeout (DNS, a refused connection) goes to your usual retry policy.
if (error.name !== "TimeoutError") throw error;
// A timed-out call may already be stored: log it and do not retry.
console.error("trial.started not confirmed");
}
if (res && res.status !== 202) {
throw new Error(`events/send answered ${res.status}: ${await res.text()}`);
}Where a missing email costs more, retry, and open the flow with a guard: a condition that ends the run when a property exists, then a step that sets it. Declared properties are strings or numbers, so the first loop declares both as numbers (a second run answers 422 for a key that exists). The request after it builds the onboarding from the introduction: a guarded welcome, three days’ wait, then a setup nudge only for contacts whose domain_verified your app has not set. A graph takes up to 150 steps.
# Once: declare the two properties the flow reads, as numbers.
for key in onboarding_started domain_verified; do
curl https://api.rasket.com/contact-properties \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-app/1.0" \
-H "Content-Type: application/json" \
-d "{ \"key\": \"$key\", \"type\": \"number\" }"
done
curl https://api.rasket.com/automations \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-app/1.0" \
-H "Content-Type: application/json" \
-d '{
"name": "Trial onboarding",
"status": "enabled",
"steps": [
{ "key": "start", "type": "trigger", "config": { "event_name": "trial.started" } },
{ "key": "seen", "type": "condition",
"config": { "type": "rule", "field": "properties.onboarding_started", "operator": "exists" } },
{ "key": "mark", "type": "contact_update",
"config": { "properties": { "onboarding_started": 1 } } },
{ "key": "welcome", "type": "send_email",
"config": { "template": { "id": "YOUR_WELCOME_TEMPLATE_ID" } } },
{ "key": "wait", "type": "delay", "config": { "duration": "3 days" } },
{ "key": "verified", "type": "condition",
"config": { "type": "rule", "field": "properties.domain_verified", "operator": "exists" } },
{ "key": "nudge", "type": "send_email",
"config": { "template": { "id": "YOUR_SETUP_NUDGE_TEMPLATE_ID" } } }
],
"connections": [
{ "from": "start", "to": "seen" },
{ "from": "seen", "to": "mark", "type": "condition_not_met" },
{ "from": "mark", "to": "welcome" },
{ "from": "welcome", "to": "wait" },
{ "from": "wait", "to": "verified" },
{ "from": "verified", "to": "nudge", "type": "condition_not_met" }
]
}'The answer is 201 with { "object": "automation", "id": "…" }. The condition has no condition_met branch, so a contact who already has the property ends there. That stops a retry arriving after the first run has passed the guard, not two copies arriving in the same moment. Marking before the send also means a run whose welcome fails is not repeated by the next event; put mark after welcome if a duplicate costs you less than a miss. Your app sets domain_verified to 1 with PATCH /contacts/{id} when the domain verifies.
How fast a run starts
Runs start on the “next tick”: a background worker picks the run up after your call returns, not inside it. Rasket publishes no start-time figure, so measure it: compare each run’s started_at from GET /automations/{id}/runs with when you sent the event.
What else can start a flow
A contact created or updated, a topic subscribed or unsubscribed, a contact entering a segment, an email event such as a bounce or a click, a store event or your custom event. The steps after it are send an email, wait, branch on a condition, update the contact, add to a segment and change a topic. No step pauses until another event arrives, but a trigger can list exit events: an abandoned-checkout flow starts on store:checkout.started, waits an hour, sends the reminder, and exits on store:order.placed if they buy first. See the automations page, and the newsletter API post for a contact, a segment and a campaign end to end.
How does an AI agent run campaigns safely?
For an assistant that runs your marketing side, connect only the audience and marketing tools (if the protocol is new to you, start with what MCP is):
claude mcp add --transport http rasket "https://api.rasket.com/mcp?toolset=audience,marketing"The consent screen then asks for scopes such as contacts:write, segments:write, topics:write, broadcasts:write, automations:write and events:send, and a tool exists only if you approved its scope. A campaign then takes four steps, and the third is yours:
create_broadcastdrafts it: a segment, a sender on a verified domain, a subject and a body. Nothing is sent.preview_broadcastreturns what the send would do: the segment with its contact counts,recipients(its subscribed contacts),from,subject, whether every sending condition passes, the content checks, and aconfirm_tokenwith itsconfirm_expires_at, an hour out. Any edit to the draft voids the token.- Your client shows you the preview and asks.
send_broadcastcarries MCP’s destructive hint, so clients that honour it prompt first; keep that prompt and never set the tool to always allow. The hint is only a hint: the server enforces the token either way. send_broadcastsends or schedules with the token. Without one, or after an edit, it is refused with a message telling the agent to preview again.
No tool deletes a campaign, a segment or an automation, or removes an unsubscribe. Grants are listed, and revoked, under Settings → Team → Authorized apps. send_event behaves like the API, so give a flow an agent triggers the guard above. The MCP reference lists every tool, and the MCP post covers connecting a client.
The MCP server takes only OAuth tokens that a person approved in a browser, and refuses API keys. A job that runs with nobody present, such as CI or a backend, calls the REST API with a key instead; if it only fires events, give it a sending_access key, which can send but cannot read or change definitions. Contact and automation webhooks carry svix-id, svix-timestamp and svix-signature headers, an HMAC-SHA256 over the id, timestamp and raw body, and a replay keeps the svix-id you dedupe on; the webhooks post verifies one in code.
How are campaigns metered next to receipts?
One monthly email allowance per plan covers campaigns, automation emails and API sends. Automation runs are not billed (fair use is 250,000 runs a month, on the automations page). Contacts are billed only while subscribed to at least one topic and not suppressed.
Take a trial-onboarding product with 8,000 contacts sending 25,000 emails a month. Pro's 100,000-email tier costs $35 with 6,750 contacts included, plus 2 started blocks of 1,000 contacts at $8.80 each: $52.60 a month. The 50,000-email tier holds the emails for $20 but includes 4,500 contacts, so it comes to $55.20. The calculator on the pricing page does this for any mix; for what two named tools would cost instead, see the platform comparison.
The API’s rate limit is 10 requests a second per team, not per key, so a sync and your password resets share it, and a 429 carries Retry-After (backoff in practice). Load a list through one POST /contacts/imports rather than a request per contact: 10,000 single writes at that rate take at least 1,000 seconds.
Nothing reserves part of the allowance for transactional mail
With pay-as-you-go off, a campaign that spends the last of it means the next password reset gets 429 monthly_quota_exceeded. Keep pay-as-you-go on, which is the default on a paid plan with a card on file, or check usage before a large send.
For reputation, send campaigns from their own subdomain, such as news.acme.example, so newsletter complaints stay mostly off the name on your receipts (subdomain guide). Campaigns also go out on their own sending stream, whose bounce and complaint rates we track apart from receipts’, but both leave from our shared pool of sending IPs unless you add a dedicated IP, from the Scale plan up (warming one up).
Which teams should pick something other than Rasket?
- Teams whose marketers test every send. If campaigns live or die on A/B tests and the people running them never touch code, a dedicated marketing platform serves them better today.
- Teams that need placement tests or engagement scoring built in. Rasket reports rates and per-recipient outcomes, and engagement segments need the workaround above.
- Flows that must pause mid-run for a reply or a second event. Exit events cover “stop when they buy”; a step that waits for an event is not available.
- Event-driven flows that cannot tolerate a duplicate. Without a key on custom events, the guard above is the best available.
- Teams that require an official client library. Coming soon: the rasket package is not published yet. Until then you write requests or generate a client from the OpenAPI document.
If none of those fit, send the order event above from a free account, then follow the newsletter API walkthrough to a first campaign.
Frequently asked questions
Can I create an automation from code instead of the dashboard?
Yes. POST /automations takes the whole graph in one request: exactly one trigger step and up to 150 steps in all, each with a key unique in the graph. GET /automations/{id}/runs then lists what each run did, and retrieving one run returns its step log. An assistant connected over MCP uses the create_automation tool for the same job.
What does a contact.updated webhook carry?
The event's type and time, and a data object with the contact's id, email, first_name, last_name, unsubscribed flag, created_at, updated_at and segment_ids. When your handler needs the contact's custom properties, fetch them with GET /contacts/{id} rather than relying on the payload.
Which webhook events can a marketing integration subscribe to?
Email events (sent, delivered, bounced, complained, opened, clicked and the rest), suppressions added and removed, contacts created, updated and deleted, automation runs started, completed and failed, plus domain, received-mail and Inbox events. The /docs/events page lists every type with an example payload.
Can I bring my existing list's consent history with me?
Imports and migrations write consent records too: their source reads import, and a record from a migration names the platform the consent was moved from. A migration also brings suppressions, lists, segments, templates and flows (as paused automations), and Rasket publishes no typical duration for one. It can only carry what the old tool kept, so check before you move that it recorded where and when each person opted in.
What DNS does a separate campaign subdomain need?
Rasket verifies the subdomain as its own sending domain: its own DKIM key, and by default a return path under it (rasket.news.acme.example for news.acme.example, or the next free label if that one is taken), so SPF and DKIM both align under relaxed alignment, the DMARC default. DMARC can stay on the parent domain: its sp= tag, or its p= when sp= is absent, sets the subdomain's policy unless you publish a record on the subdomain itself.
Sources
- Guidelines 05/2020 on consent under Regulation 2016/679 — European Data Protection Board, read 2026-10-17
- RFC 8058: Signaling One-Click Functionality for List Email Headers — RFC Editor, read 2026-10-17
- Email sender guidelines — Google, read 2026-10-17
- Sender Best Practices — Yahoo, read 2026-10-17
- mustache(5): Logic-less templates — Mustache, read 2026-10-17
- Model Context Protocol specification: Tools — Model Context Protocol, read 2026-10-17
- Events API reference — Rasket, read 2026-10-17
- Rate limits — Rasket, read 2026-10-17
- MCP server reference — Rasket, read 2026-10-17
- Automations API reference — Rasket, read 2026-10-17
- Automations — Rasket, read 2026-10-18
- Pricing — Rasket, read 2026-10-17
Related
- Newsletter API: send campaigns from your code — Send a newsletter from code or an AI agent: contact, segment, draft, test, human approval, then send. Runnable Node scripts and the MCP flow, with real output.
- Email marketing platforms for small business — Six email marketing platforms for small business priced at 500, 1,000 and 5,000 contacts from vendors' own pages, plus Shopify's tool and where Rasket loses.
- Should you use an email subdomain for marketing? — When a marketing subdomain like news.example.com is worth it, by volume: what it separates from your domain, what it shares, and the DMARC and DNS records.
- Automations — Workflows that run per contact: start on a custom event or a segment change, wait, branch, and send. Every run pins a version and is inspectable.
- Let an AI agent send email with MCP — Connect an agent to an MCP email server: the OAuth sign-in for your own loop, the send_email call, prompt injection, and a person approving every send.
- Marketing email — An email marketing platform on the API you already send with: campaigns to contacts and segments, automations from your product's events, a brand-aware editor.