Campaigns
Reading needs Campaigns: view, every change Campaigns: manage.
What the API can do
Section titled “What the API can do”The API creates regular campaigns with Custom HTML content. A/B, multivariate and resend campaigns, the drag & drop and the simple editor, and templates are made in the app. The API still lists and reads every campaign, and schedules, cancels or deletes them.
| Method and path | What it does |
|---|---|
GET /campaigns |
Lists campaigns, newest change first. |
POST /campaigns |
Creates a draft. |
GET /campaigns/{id} |
Reads one campaign (with its content if it is Custom HTML). |
PATCH /campaigns/{id} |
Changes a draft. |
DELETE /campaigns/{id} |
Deletes a campaign. |
POST /campaigns/{id}/test |
Sends a test email. |
POST /campaigns/{id}/schedule |
Sends now or schedules a draft. |
POST /campaigns/{id}/cancel |
Takes a scheduled campaign back, or stops one that is sending. |
GET /campaigns/{id}/activity |
The recipients and what they did. |
The campaign object
Section titled “The campaign object”{ "id": "30bc6133-16ad-4074-b311-204127a80b62", "name": "Winter-ready garden (EN)", "type": "regular", "status": "sent", "editor": "dragdrop", "subject": "Get your garden ready for winter", "from_name": "Kertlak Garden Shop", "recipients": {"all": false, "groups": ["5e30c6ff-0462-47d1-9a6c-7db4c33d6b9a"], "segments": [], "exclude_groups": [], "exclude_segments": [], "has_rules": false}, "delivery": "instant", "scheduled_for": null, "created_at": "2026-10-15T07:00:00Z", "updated_at": "2026-10-20T08:02:25.333333Z", "sent_at": "2026-10-20T08:02:25.333333Z", "stats": {"recipients": 1272, "sent": 1272, "delivered": 1266, "opened": 337, "clicked": 57, "unsubscribed": 4, "bounced": 0, "soft_bounced": 6, "complaints": 0, "open_rate": 26.49, "click_rate": 4.48}}| Key | Meaning |
|---|---|
type |
regular, ab, multivariate or resend. Campaigns made in the app are read without their type’s own settings. |
status |
draft, ready (scheduled, in the Outbox), sending, paused, sent or canceled. |
editor |
dragdrop, simple, html (Custom HTML) or null (no content yet). |
recipients |
all (every active subscriber) or the group and segment lists, with exclusions. has_rules is true when the recipients are chosen by a rule set built in the app; the lists are then empty. |
delivery |
instant, scheduled, timezone_based, or null for a draft. |
scheduled_for |
When sending starts, in UTC. For timezone_based, when the first time zone reaches the chosen local time. |
sent_at |
When a sent campaign finished sending. |
stats |
opened and clicked are unique; bounced is permanent bounces; open_rate and click_rate are percentages of sent. |
content |
Only in single-campaign answers of a Custom HTML campaign: html and text. |
List campaigns
Section titled “List campaigns”| Parameter | Meaning |
|---|---|
status |
draft, outbox (ready, sending, paused) or sent (sent, canceled). Without it: every campaign. |
type |
regular, ab, multivariate, resend. |
search |
Name or subject contains. |
limit, cursor |
Paging. total is always there. |
curl -H "Authorization: Bearer $ECHO_TOKEN" "$ECHO_API/campaigns?status=outbox"{"data":[{"id":"f8ab784a-0a6a-45f0-8dd8-38ecd146826a","name":"Novemberi hírlevél: téli madáretetés","type":"regular","status":"ready","editor":"dragdrop","subject":"Téli madáretetés: mikor kezdjük és mivel?","from_name":"Kertlak Webshop","from_email":"[email protected]","reply_to":"[email protected]","recipients":{"all":false,"groups":["d5478c4a-97d3-4fa9-b38d-dd32c41dbac3"],"segments":[],"exclude_groups":[],"exclude_segments":[],"has_rules":false},"delivery":"scheduled","scheduled_for":"2026-11-05T08:00:00Z","created_at":"2026-10-19T05:20:29.127968Z","updated_at":"2026-10-21T09:20:29.127968Z","sent_at":null,"stats":{"recipients":0,"sent":0,"delivered":0,"opened":0,"clicked":0,"unsubscribed":0,"bounced":0,"soft_bounced":0,"complaints":0,"open_rate":0,"click_rate":0}}],"meta":{"next_cursor":null,"prev_cursor":null,"limit":25,"total":1}}Lists never contain content.
Create a draft
Section titled “Create a draft”POST /campaigns. name and content.html are required.
| Key | Meaning |
|---|---|
name |
Internal name (up to 255 characters). |
subject, from_name, from_email, reply_to |
The email’s details. Without them the workspace’s defaults are used. from_email must be on a verified sending domain; this is checked when you schedule, not before. |
recipients |
all, groups, segments, exclude_groups, exclude_segments (IDs). Replaces the lists. |
content.html |
The Custom HTML. It is sanitized as in the app, and every merge tag in it must exist. |
content.text |
The plain text version. Without it, Echo makes one from the HTML. |
curl -X POST "$ECHO_API/campaigns" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Őszi kertápolás","subject":"Őszi kertápolás: öt tipp","from_name":"Kertlak Webshop","from_email":"[email protected]","recipients":{"groups":["d5478c4a-97d3-4fa9-b38d-dd32c41dbac3"]},"content":{"html":"<h1>Kedves {$name},</h1><p>itt az őszi kertápolási tippsorunk.</p><p><a href=\"{$unsubscribe}\">Leiratkozás</a></p>"}}'201 Created:
{"data":{"id":"899d27ec-f6f3-4551-a7c5-7fc993cd4002","name":"Őszi kertápolás","type":"regular","status":"draft","editor":"html","subject":"Őszi kertápolás: öt tipp","from_name":"Kertlak Webshop","from_email":"[email protected]","reply_to":"[email protected]","recipients":{"all":false,"groups":["d5478c4a-97d3-4fa9-b38d-dd32c41dbac3"],"segments":[],"exclude_groups":[],"exclude_segments":[],"has_rules":false},"delivery":null,"scheduled_for":null,"created_at":"2026-10-22T06:08:40.347097Z","updated_at":"2026-10-22T06:08:40.363886Z","sent_at":null,"stats":{"recipients":0,"sent":0,"delivered":0,"opened":0,"clicked":0,"unsubscribed":0,"bounced":0,"soft_bounced":0,"complaints":0,"open_rate":0,"click_rate":0},"content":{"html":"\u003ch1\u003eKedves {$name},\u003c/h1\u003e\u003cp\u003eitt az őszi kertápolási tippsorunk.\u003c/p\u003e\u003cp\u003e\u003ca href=\"{$unsubscribe}\"\u003eLeiratkozás\u003c/a\u003e\u003c/p\u003e","text":"***************\nKedves {$name},\n***************\n\nitt az őszi kertápolási tippsorunk.\n\nLeiratkozás ( {$unsubscribe} )"}}}Merge tags are the fields’ tags ({$name}, {$city}, your custom fields) and the system links {$unsubscribe}, {$preferences}, {$webview} and {$forward}. An unknown tag:
{"error":{"code":"unknown_tags","message":"Unknown personalization tags: {$keresztnev}","fields":{"content.html":"Unknown personalization tags: {$keresztnev}"}}}If a step after creating the draft fails (for example the content), the draft is deleted again and the answer is that step’s error.
Get and change a draft
Section titled “Get and change a draft”GET /campaigns/{id} returns the campaign; a Custom HTML campaign also has content.
PATCH /campaigns/{id} changes the given keys of a draft (same keys as create). content replaces the whole content, and is accepted for Custom HTML campaigns and for drafts without content yet.
curl -X PATCH "$ECHO_API/campaigns/899d27ec-f6f3-4551-a7c5-7fc993cd4002" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \ -d '{"subject":"Őszi kertápolás: öt tipp a hidegek előtt"}'| Error | When |
|---|---|
409 not_editable |
The campaign isn’t a draft (also for a rename), or you sent content for a campaign made in another editor. |
409 editor_chosen |
Another request chose a different editor for this draft at the same moment. |
PATCH isn’t atomic: the details are saved before the content, so a content error can leave the other changes saved.
Send a test email
Section titled “Send a test email”POST /campaigns/{id}/test sends the campaign to 1 to 5 addresses, with [Test] before the subject. The sender must be on a verified sending domain and the content must be set.
curl -X POST "$ECHO_API/campaigns/899d27ec-f6f3-4551-a7c5-7fc993cd4002/test" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \202 Accepted, empty body. A workspace can send 50 test emails in any 24 hours, the app’s tests included; over that the answer is 422.
Schedule and cancel
Section titled “Schedule and cancel”POST /campaigns/{id}/schedule takes a draft out of the drafts:
| Key | Meaning |
|---|---|
delivery |
instant (send now), scheduled or timezone_based (each subscriber at the same local time). |
date |
YYYY-MM-DD, for scheduled and timezone_based. |
time |
HH:MM, in 15-minute steps, for scheduled and timezone_based. |
timezone |
An IANA name from GET /timezones, for scheduled. |
The app’s pre-send checks run first: a sender on a verified sending domain, the company details, an unsubscribe link in the content (unless the automatic footer is on) and recipients. A campaign that can’t be sent answers 422 validation with the fields. Times are at most a year ahead; timezone_based works for regular campaigns at least 24 hours ahead.
curl -X POST "$ECHO_API/campaigns/899d27ec-f6f3-4551-a7c5-7fc993cd4002/schedule" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \ -d '{"delivery":"scheduled","date":"2027-09-30","time":"09:00","timezone":"Europe/Budapest"}'200 OK with the campaign, now "status":"ready", "delivery":"scheduled" and "scheduled_for":"2027-09-30T07:00:00Z". A time off the 15-minute grid:
{"error":{"code":"validation","message":"Please check the highlighted fields.","fields":{"schedule":"Choose a time in the future, in 15-minute steps."}}}instant really starts sending to every recipient. Use a test email or a small test group first.
POST /campaigns/{id}/cancel (no body): a scheduled campaign goes back to the drafts ("status":"draft", "delivery":null); a sending or paused one stops and becomes canceled. Any other status is 409 not_editable.
curl -X POST "$ECHO_API/campaigns/899d27ec-f6f3-4551-a7c5-7fc993cd4002/cancel" -H "Authorization: Bearer $ECHO_TOKEN"Delete a campaign
Section titled “Delete a campaign”curl -X DELETE "$ECHO_API/campaigns/899d27ec-f6f3-4551-a7c5-7fc993cd4002" -H "Authorization: Bearer $ECHO_TOKEN"204 No Content. A sending or paused campaign must be canceled first (409 not_editable), and a campaign that was sent within the last 30 days can’t be deleted yet (409 recently_sent).
Recipient activity
Section titled “Recipient activity”GET /campaigns/{id}/activity lists the recipients of a campaign that has started sending (an earlier one answers an empty page).
| Parameter | Meaning |
|---|---|
type |
all (default), opened, clicked, forwarded, unopened, unsubscribed, hard_bounce, soft_bounce, complaints. |
search |
Email address contains (needs Subscribers: view). |
limit, cursor |
Paging. |
curl -H "Authorization: Bearer $ECHO_TOKEN" \ "$ECHO_API/campaigns/30bc6133-16ad-4074-b311-204127a80b62/activity?type=clicked&limit=2"{"data":[{"subscriber":{"id":"04393fa0-81ff-44a3-82e2-67af141df99a","email":"[email protected]"},"opens":1,"clicks":1,"sent_at":"2026-10-20T08:00:23.444444Z","opened_at":"2026-10-20T08:34:16.826706Z","clicked_at":"2026-10-20T08:47:18.826706Z"},{"subscriber":{"id":"f56a4743-a5db-4d64-b9dc-bb69afdf0659","email":"[email protected]"},"opens":1,"clicks":1,"sent_at":"2026-10-20T08:02:19.666667Z","opened_at":"2026-10-20T09:58:56.816405Z","clicked_at":"2026-10-20T10:09:11.816405Z"}],"meta":{"next_cursor":"cDoy","prev_cursor":null,"limit":2,"total":57}}opened_at and clicked_at are the first open and click. Without Subscribers: view the subscriber object has only id.