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
/api/v1/kahunacart/newsletters/campaigns
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
/api/v1/kahunacart/newsletters/campaigns/{id}
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
/api/v1/kahunacart/newsletters/campaigns/audience
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
/api/v1/kahunacart/newsletters/campaigns
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
/api/v1/kahunacart/newsletters/campaigns/{id}
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
/api/v1/kahunacart/newsletters/campaigns/{id}
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
/api/v1/kahunacart/newsletters/campaigns/{id}/sends
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. |
List a campaign's links
/api/v1/kahunacart/newsletters/campaigns/{id}/links
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
/api/v1/kahunacart/newsletters/campaigns/{id}/timeline
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
/api/v1/kahunacart/newsletters/campaigns/{id}/preview
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
/api/v1/kahunacart/newsletters/campaigns/{id}/test
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
/api/v1/kahunacart/newsletters/campaigns/{id}/start
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
/api/v1/kahunacart/newsletters/campaigns/{id}/pause
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
/api/v1/kahunacart/newsletters/campaigns/{id}/resume
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
/api/v1/kahunacart/newsletters/campaigns/{id}/cancel
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
/api/v1/kahunacart/newsletters/campaigns/{id}/catch-up
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
/api/v1/kahunacart/newsletters/campaigns/{id}/catch-up
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
/api/v1/kahunacart/newsletters/campaigns/{id}/retry-failed
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
/api/v1/kahunacart/newsletters/campaigns/{id}/preview
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
/api/v1/kahunacart/newsletters/campaigns/{id}/sends/{send}/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.