Skip to content

Powered by Grav CMS + Helios Theme

Newsletter campaigns (add-on)

Writing, previewing, test-sending, starting, pausing and stopping a campaign, and what each one did.

Provided by the KahunaCart Newsletters plugin. Reading takes kahunacart.newsletters.view and writing takes kahunacart.newsletters.manage. Starting, resuming, catching up or test-sending a campaign also needs kahunacart.newsletters.send, because a message that has gone out cannot be taken back. What each action does is on Campaigns.

List campaigns

GET /api/v1/kahunacart/newsletters/campaigns Permission kahunacart.newsletters.view

Every campaign, newest first, with its status, the list it goes to, its counters and its open and click rates. The status counts come back for the whole table rather than for the page, so "four drafts" means four in the store. Filter by status to answer questions like "what is scheduled" or "what stopped itself".

Parameters

Name Type Description
status optional string Only campaigns in this status
One of: draft, scheduled, sending, paused, sent, cancelled, failed
sort optional string Sort by this column instead of newest first
One of: name, status, sent, open_rate, click_rate, when
dir optional string Which way sort runs. Without it, dates and counts run highest first and words A to Z
One of: asc, desc
q optional string Words to search for in the campaign name and subject; every word has to be there, wherever it sits. Applied before the sort and the page, and total counts what it found
page optional integer Page number, from 1
per_page optional integer Rows per page

Get one campaign

GET /api/v1/kahunacart/newsletters/campaigns/{id} Permission kahunacart.newsletters.view

One campaign with everything above its tables: the subject, the preheader, the Markdown body, the layout, the list, the sender overrides, the coupon, whether opens and clicks are counted, every counter, the open and click rates, and a breakdown of its recipients by what happened to each of them. On a campaign testing two subjects it also answers the test: both subjects, how big the sample is, when the winner is picked, which one won and when, and each letter's own sent, opened and clicked counts — always the sample's, never the whole campaign's. Also answers which moves the state machine will accept from here, so a caller knows whether it can be started, paused or edited before it tries.

Parameters

Name Type Description
id required integer Campaign id

Count who a campaign would reach

GET /api/v1/kahunacart/newsletters/campaigns/audience Permission kahunacart.newsletters.view

How many people a campaign to these lists, narrowed by this segment, would mail right now, and up to twenty of them by name, before any campaign exists. Writes nothing. The same walk the send does, so somebody on two of the lists is counted once, and anybody suppressed, anybody who left and anybody who never confirmed is counted separately rather than offered. With no lists and a segment, it counts everybody on any list who matches the segment.

Parameters

Name Type Description
lists optional string List ids, comma separated, such as "1,3"
segment optional integer A segment id

Write a campaign

POST /api/v1/kahunacart/newsletters/campaigns Permission kahunacart.newsletters.manage

Create a campaign in draft. Nothing is sent and nothing is queued: a new campaign always starts as a draft and has to be started separately, which is a different permission. The body is Markdown, wrapped in the store's own email layout. Merge tags are checked here rather than at send time, so a campaign carrying a variable that is not allowed is refused now with the variable named. Pass a template id to start from a saved template, which copies its fields once.

Parameters

Name Type Description
name required string What the store calls it. Nobody outside sees this.
subject optional string The subject line, which may carry merge tags
preheader optional string or null The line an inbox shows after the subject
body_md optional string or null The message, as Markdown
layout optional string store wraps it in the store's branded email layout; plain is white ground and no card
One of: store, plain
list_id optional integer or null The list to send it to
list_ids optional array of integer Every list to send it to, in order, instead of list_id. Somebody on two of them is mailed once. Leave it empty and give a segment_id to send to everybody on any list who matches the segment.
segment_id optional integer or null Narrow it to the people who match this segment. A segment tied to a list needs that list among the campaign's lists.
template_id optional integer Start from this template, copying its fields once
from_name optional string or null Overrides the store default
from_email optional string or null Overrides the store default
reply_to optional string or null
coupon_code optional string or null Adds coupon.code and coupon.url to the merge tags
track_opens optional boolean Defaults to the store setting
track_clicks optional boolean Defaults to the store setting
subject_b optional string or null A second subject line, which turns this campaign into a subject test. A sample of the audience gets one of the two, and after the window everybody else gets whichever was opened more — or clicked more, on a campaign that is not counting opens. Only the subject differs between the two copies. Give this on its own and the sample and the window fall back to 20 percent over four hours.
ab_sample_percent optional integer or null How much of the audience the subject test is run on, between 10 and 50. Below ten the answer is noise on any list small enough to care about the difference; at fifty the sample is half the campaign.
ab_window_minutes optional integer or null How long to wait, in minutes, before the winner goes out to everybody else. Counted from the moment the campaign starts. The admin offers 1, 2, 4, 8 and 24 hours; any positive number is accepted here.
utm optional object This campaign's own link tagging, overriding the store defaults. Only links that point back at this store are tagged, and anything already tagged in the message is left alone. Any of the four left empty falls back to the store default, and an empty campaign falls back to a slug of this campaign's name. Keys: `source`, `medium`, `campaign`, `content`

Edit a campaign

PATCH /api/v1/kahunacart/newsletters/campaigns/{id} Permission kahunacart.newsletters.manage

Change a campaign that has not gone anywhere yet. Allowed from draft and scheduled only: a campaign that is sending has already put its subject in somebody's inbox, and editing it would mean two halves of one audience receiving two different messages under one name. Anything else is refused with a sentence saying so. Merge tags are checked against the campaign as it would stand afterwards, so a bad tag left in the body is reported even when only the subject was edited.

Parameters

Name Type Description
id required integer Campaign id
name optional string
subject optional string
preheader optional string or null
body_md optional string or null
layout optional string
One of: store, plain
list_id optional integer or null
list_ids optional array of integer Every list, in order. Replaces the lists it had. list_id on its own sets one list.
segment_id optional integer or null Null clears it
from_name optional string or null
from_email optional string or null
reply_to optional string or null
coupon_code optional string or null
track_opens optional boolean
track_clicks optional boolean
subject_b optional string or null The second subject line. Null clears it and the campaign goes out with one subject and no test. Only editable while the campaign is a draft or scheduled, like everything else here.
ab_sample_percent optional integer or null How much of the audience the test runs on
ab_window_minutes optional integer or null How long to wait before the winner goes out, in minutes
utm optional object This campaign's link tagging. Sending an object with empty values clears every override and puts the campaign back on the store's own defaults. Keys: `source`, `medium`, `campaign`, `content`

Delete a campaign

DELETE /api/v1/kahunacart/newsletters/campaigns/{id} Permission kahunacart.newsletters.manage

Remove a campaign and everything it recorded. Only a draft or a campaign that was canceled can be deleted; anything that has sent a message is refused, because its recipient list is the only record of who received it and that is the question a complaint always asks.

Parameters

Name Type Description
id required integer Campaign id

List a campaign's recipients

GET /api/v1/kahunacart/newsletters/campaigns/{id}/sends Permission kahunacart.newsletters.view

Who this campaign was sent to and what happened to each of them, a page at a time. On a campaign testing two subjects each row also carries the letter that person was actually sent, A or B. With the status counts for the whole campaign rather than for the page. Filter by status to answer the two questions that come up: which addresses failed and why (the transport's own words are on the row), and who was excluded before anything was sent — suppressed, already off the list, or never confirmed.

Parameters

Name Type Description
id required integer Campaign id
status optional string Only recipients in this state
One of: pending, sent, delivered, opened, clicked, bounced, complained, failed, skipped
sort optional string Sort by this column instead of the sequence they were written in
One of: email, status, clicks, when
dir optional string Which way sort runs. Without it, dates and counts run highest first and words A to Z
One of: asc, desc
q optional string Words to search for in the recipient address, message id and error; every word has to be there, wherever it sits. Applied before the sort and the page, and total counts what it found while counts stay for the whole campaign
page optional integer At least 1.
per_page optional integer At least 1, at most 200.
GET /api/v1/kahunacart/newsletters/campaigns/{id}/links Permission kahunacart.newsletters.view

Every link in the campaign with how many times it was pressed and what share of the people it reached that is, ordered by clicks so the link that worked is at the top rather than buried under the header image nobody pressed. Empty until the campaign has been rendered for sending, because that is when the links are recorded.

Parameters

Name Type Description
id required integer Campaign id

Get a campaign's first 48 hours

GET /api/v1/kahunacart/newsletters/campaigns/{id}/timeline Permission kahunacart.newsletters.view

Four cumulative series — sent, delivered, opened, clicked — in one-hour buckets from the moment the first message went out, over the first forty-eight hours. Cumulative rather than per hour, so every line only goes up and how it climbs is the answer. Use it to tell a subject line that landed from one that was opened out of habit three days later.

Parameters

Name Type Description
id required integer Campaign id

Preview a campaign

POST /api/v1/kahunacart/newsletters/campaigns/{id}/preview Permission kahunacart.newsletters.manage

Render a campaign the way one person would receive it, through the real layout with the real Markdown and the real merge pass, and answer the HTML, the plain-text part and the subject. Tracking is off, so nothing here is counted and no link is rewritten. Draft fields in the body override the saved ones, so an unsaved edit can be previewed without being saved first. Any merge tag the sandbox will not allow comes back in the refusals list beside the render rather than instead of it.

Parameters

Name Type Description
id required integer Campaign id
subject optional string Override the saved subject for this render
preheader optional string or null
body_md optional string or null
layout optional string
One of: store, plain
coupon_code optional string or null

Send a test copy

POST /api/v1/kahunacart/newsletters/campaigns/{id}/test Permission kahunacart.newsletters.send

Mail one copy of a campaign to an address, with the subject prefixed and nothing counted, so it can be looked at in a real inbox before anybody else sees it. It goes through the store's real transport, which is the point of it, so this needs the send permission rather than the manage one. An address that is already a real subscriber is used as it stands, links and all; a genuinely new address is written down and flagged as a test so it never enters an audience.

Parameters

Name Type Description
id required integer Campaign id
email required string Where the test copy goes

Send or schedule a campaign

POST /api/v1/kahunacart/newsletters/campaigns/{id}/start Permission kahunacart.newsletters.send

Put this campaign in inboxes, now or at a moment. The audience is worked out first, so a campaign that would reach nobody is refused before anything is queued, and everybody excluded gets a row saying why. A scheduled_at in the future schedules it; one in the past or omitted sends it now. This is a one-way door — pausing stops what has not gone and does not get back what has — which is why sending is a permission of its own.

Parameters

Name Type Description
id required integer Campaign id
scheduled_at optional integer UTC epoch seconds to start at. Omit, or pass a moment already past, to send now.

Pause a campaign

POST /api/v1/kahunacart/newsletters/campaigns/{id}/pause Permission kahunacart.newsletters.manage

Stop a sending or scheduled campaign within one message. The running job is asked to cancel before the status is written, so there is no window in which the campaign says paused while its job is still delivering. Resuming later carries on from what is still pending, and nobody is mailed twice.

Parameters

Name Type Description
id required integer Campaign id

Resume a campaign

POST /api/v1/kahunacart/newsletters/campaigns/{id}/resume Permission kahunacart.newsletters.send

Carry on with a paused campaign, or restart one that stopped itself after too many refused messages. A fresh job is queued rather than the old one revived, and it claims only the recipients still waiting — the unique constraint on the recipient row is what makes that safe. Needs the send permission, because this is what puts the remaining mail in the remaining inboxes.

Parameters

Name Type Description
id required integer Campaign id

Cancel a campaign

POST /api/v1/kahunacart/newsletters/campaigns/{id}/cancel Permission kahunacart.newsletters.manage

Stop a campaign for good. It cannot be started again, and whoever has already received it keeps it. The recipients still waiting are left in place rather than deleted, because "who did this campaign not reach" is the question somebody asks the morning after a cancellation and those rows are the only answer.

Parameters

Name Type Description
id required integer Campaign id

Send a campaign to the people who joined late

POST /api/v1/kahunacart/newsletters/campaigns/{id}/catch-up Permission kahunacart.newsletters.send

Send a campaign that has already gone out to the people who joined its list afterwards and never received it. The audience is worked out again, which adds only the people who have joined since and who match the campaign's segment, and leaves everybody already mailed exactly as they are — so it stays one campaign with one report rather than a duplicate pointed at a segment. Nobody gets a second copy, a campaign that gives everybody their own coupon code mints a new one for each newcomer, and a campaign whose subject test already picked a winner sends the winner rather than re-opening the test. Only a sent campaign can do this, and it is refused when nobody has joined since. Needs the send permission, because it is what puts the mail in the inboxes.

Parameters

Name Type Description
id required integer Campaign id

Count who joined after a campaign went out

GET /api/v1/kahunacart/newsletters/campaigns/{id}/catch-up Permission kahunacart.newsletters.view

How many people would be mailed if this campaign were sent on to the people who joined its list after it went out, and up to twenty of them by name. Writes nothing. The same walk the send itself does, so the number here is the number that would be written: everybody on the list — and in the segment, where the campaign names one — who has no recipient row on this campaign yet, with anybody suppressed, anybody who left the list and anybody who never confirmed counted separately rather than offered.

Parameters

Name Type Description
id required integer Campaign id

Retry a campaign's failed sends

POST /api/v1/kahunacart/newsletters/campaigns/{id}/retry-failed Permission kahunacart.newsletters.send

Try the failed sends of a campaign that has gone out again: every failed send, or only the one named by send_id. A failed send is one the mailer refused, usually because the transport could not be reached for a moment; bounces and complaints are never retried. The campaign runs again as the same campaign with the same report, nobody already mailed is mailed twice, and consent and suppression are checked again at delivery. Only a sent campaign can do this, and it is refused when nothing failed. Needs the send permission.

Parameters

Name Type Description
id required integer Campaign id
send_id optional integer One failed send to retry; omit to retry every failed send

Read a saved campaign preview

GET /api/v1/kahunacart/newsletters/campaigns/{id}/preview Permission kahunacart.newsletters.view

The campaign as it is saved, rendered through the real layout, for a reader without the manage permission and for the read-only demo. The POST on the same path renders unsaved fields as well.

Read one recipient's copy of a campaign

GET /api/v1/kahunacart/newsletters/campaigns/{id}/sends/{send}/view Permission kahunacart.newsletters.view

One person's copy of a campaign that went out, rendered from the campaign's snapshot and their send row: the subject line they were sent, their name and any code minted for them, beside the send row. Tracking is off and every link in it is inert, so viewing is never an open or a click. A send is only found through its own campaign. No tool: it holds a person's name and code, which is not something to hand a model by accident. The campaign as a whole is the preview.

Times are UTC epoch seconds, both ways. A campaign can be edited only while it is a draft or scheduled; an edit to one that has started is refused with a sentence saying why.