Backlog API
Notify
Read contacts and segments, write mailing drafts, send test mails, and send or schedule a mailing to customers.
Base URLhttps://backlog.polder.work
Notify sends mailings to the contacts of your customers. The API does what the screens do, through the same actions and the same rules.
Scopes and who may do what
Three scopes, each a bigger step than the last:
| Scope | What it allows |
|---|---|
notify:read |
Read categories, contacts, segments, audiences and mailings (with their results). Contacts are other people's addresses, so this is its own scope. |
notify:write |
Create and change drafts, send test mails. Nothing reaches a customer. |
notify:send |
Send and schedule a mailing. This cannot be taken back, so no token has it unless somebody ticked it. |
A scope only narrows what the token's owner may do. On top of it:
- A guest (a client) gets
403everywhere, and so does a workspace that has switched Notify off. - A member may read, draft and send test mails to themselves or a colleague.
- Only a workspace administrator may send or schedule, and send a test mail to any address.
The MCP server offers the read and draft tools (polder_list_mailings, polder_create_mailing_draft, …) but never sending:
an assistant prepares a draft and a person sends it. See Scopes.
A mailing
A mailing is a draft until somebody sends it.
| Field | Description |
|---|---|
id |
Mailing id. |
subject, preheader |
Subject line and preview text. |
status |
draft, scheduled, sending, paused, sent, cancelled or failed. |
category_id |
The mail category (mail_category_id when you write it). |
include_segment_ids, exclude_segment_ids |
Segments whose contacts receive it, and segments whose contacts do not. |
track |
Whether opens and clicks are measured. |
reply_team_id |
The team that receives replies. |
recipients_total |
Number of recipients, set at the moment sending starts. |
scheduled_at, started_at, finished_at, created_at, updated_at |
ISO 8601 or null. |
body |
The HTML body. Only returned when you read or write a single mailing. |
List categories
/api/v1/workspaces/{workspace}/notify/categoriesThe workspace's mail categories. A mailing needs one; an unsubscribable category is one that recipients can opt out of.
curl https://backlog.polder.work/api/v1/workspaces/7/notify/categories \
-H "Authorization: Bearer $POLDER_TOKEN"
{ "data": [{ "id": 2, "name": "Product news", "unsubscribable": true }] }
List contacts
/api/v1/workspaces/{workspace}/notify/contactsThe contacts of the workspace's customers, ordered by name and paged.
| Parameter | Description |
|---|---|
q |
Search in the contact's name, email address and customer name. |
page, per_page |
Paging. per_page defaults to 50, at most 200. |
{
"data": [{
"id": 31, "name": "Sanne de Vries", "email": "sanne@example.com", "language": "nl", "role": "Owner",
"status": "…", "customer": { "id": 4, "name": "Acme" }
}],
"meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}
List segments
/api/v1/workspaces/{workspace}/notify/segmentsAll segments of the workspace, ordered by name. kind and filters describe how the segment picks its customers;
a segment that lists customers or contacts by hand also has customer_ids or contact_ids.
{ "data": [{ "id": 5, "name": "Dutch customers", "kind": "…", "filters": {} }] }
Preview a segment's audience
/api/v1/workspaces/{workspace}/notify/segments/{segment}/audienceWho one segment reaches for a category right now: the counts and why people fall away, plus a sample. The addresses of everybody are never listed.
| Parameter | Required | Description |
|---|---|---|
category_id |
yes | A category of this workspace. Unsubscribes depend on the category. |
A segment of another workspace is a 404.
{
"data": {
"customers": 12, "contacts": 40, "reachableContacts": 38, "reachableCustomers": 12,
"dropped": { "unsubscribed": 1, "suppressed": 1, "expired": 0, "customersWithoutAddress": 0 },
"sample": [{ "id": 31, "name": "Sanne de Vries", "email": "sanne@example.com", "customer": "Acme", "blocked": null }]
}
}
sample holds at most 50 contacts; blocked says why a contact is dropped, or is null.
List mailings
/api/v1/workspaces/{workspace}/notify/mailingsMailings, newest first, paged. Filter with ?status= (one of the statuses above), page with page and per_page.
The body is left out of the list.
{
"data": [{ "id": 12, "subject": "Maintenance on Friday", "status": "draft", "…": "…" }],
"meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}
Get a mailing
/api/v1/workspaces/{workspace}/notify/mailings/{mailing}One mailing with its body and stats. A mailing from another workspace is a 404.
stats counts recipients: sent, pending, failed, cancelled, delivered, bouncedHard, bouncedSoft,
complaints, unsubscribed, replies, and for tracking opened, clicked, clicks and botEvents.
Note
openedis an upper bound. Mail clients that preload images and link scanners show up as opens; those are counted separately asbotEventswhere Polder recognises them.
{
"data": {
"id": 12, "subject": "Maintenance on Friday", "status": "sent", "recipients_total": 38,
"body": "<p>Hello {{naam}},</p>…",
"stats": { "sent": 38, "delivered": 37, "bouncedHard": 1, "opened": 21, "clicked": 4, "…": "…" }
}
}
Create a draft
/api/v1/workspaces/{workspace}/notify/mailingsCreates a mailing in status draft. Every field is optional, so you can start empty and fill it in later. Sending
checks that subject, text, category and at least one included segment are present. The mailing starts with the
workspace's default for track, and replies go to the team of the workspace's catch-all mailbox until you set
reply_team_id. Answers 201 with the mailing, body included.
Body
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | no | At most 200 characters. |
preheader |
string | no | At most 200 characters. |
body |
string | no | Simple HTML, at most 300,000 characters. {{naam}} and {{klant}} are filled in per recipient. |
mail_category_id |
integer | no | A category of this workspace. |
include_segment_ids |
integer[] | no | Segments of this workspace. |
exclude_segment_ids |
integer[] | no | Segments of this workspace. |
track |
boolean | no | Measure opens and clicks. |
reply_team_id |
integer | no | A team you may see. |
curl https://backlog.polder.work/api/v1/workspaces/7/notify/mailings \
-H "Authorization: Bearer $POLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject": "Maintenance on Friday", "body": "<p>Hello {{naam}},</p>", "mail_category_id": 2, "include_segment_ids": [5]}'
Update a draft
/api/v1/workspaces/{workspace}/notify/mailings/{mailing}Changes the fields you send, with the same fields and rules as creating. Only a draft or a scheduled mailing can change;
anything else answers 409.
Really changing a scheduled mailing takes it back to draft, so what goes out is always what somebody confirmed. The
response then has unscheduled: true. Saving the same values again keeps the schedule.
{ "data": { "id": 12, "status": "draft", "…": "…", "unscheduled": true } }
Send a test mail
/api/v1/workspaces/{workspace}/notify/mailings/{mailing}/testSends the mailing to one address, exactly as recipients get it, with [Test] in front of the subject. It never touches
the recipient list. The address must be yourself or a member of the workspace; an administrator may use any address.
Besides the route's limit, each person gets at most 5 test mails a minute (NOTIFY_TESTS_PER_MINUTE).
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | yes | Email address. |
{ "data": { "ok": true, "error": null } }
If the mail provider refuses the message the call still answers 200 with ok: false and the provider's error.
| Status | reason |
Meaning |
|---|---|---|
403 |
forbidden |
No right to use Notify here. |
422 |
invalid_address |
to is not an email address. |
422 |
address_not_allowed |
Not you or a member, and you are not an administrator. |
422 |
no_connection |
The workspace has no sending connection. |
429 |
rate_limited |
Too many test mails; context.seconds says how long to wait. |
Send a mailing
/api/v1/workspaces/{workspace}/notify/mailings/{mailing}/sendSends a draft (or a scheduled mailing) now. Polder checks everything again at this moment, freezes the audience,
writes a line in the contact log of every customer reached, and hands the mailing to the queue. The answer is 202 with
the mailing in status sending; follow its progress with Get a mailing.
Confirm the recipient count
Sending cannot be undone, so you must say how many people you expect to mail, the way the screen shows the number.
| Field | Type | Required | Description |
|---|---|---|---|
count |
integer | yes | The number of reachable contacts you were shown. |
typed |
integer | from the threshold | From 50 recipients on (notify.confirm_threshold) the number must be given a second time, as the screen asks somebody to type it over. |
If the audience is not count any more (somebody unsubscribed, a segment changed) nothing is sent and the response
tells you the real number. You can use that to find out the count: send 0 and read context.count from the
audience_changed answer. For several segments or an exclusion list that is the only way to see the number for the
whole mailing; the segment audience endpoint counts one segment at a time.
curl https://backlog.polder.work/api/v1/workspaces/7/notify/mailings/12/send \
-H "Authorization: Bearer $POLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"count": 118, "typed": 118}'
{ "message": "audience_changed", "reason": "audience_changed", "context": { "count": 117 } }
Errors
Refusals have the shape above, with a stable reason and context holding what you need to try again.
| Status | reason |
Meaning |
|---|---|---|
403 |
forbidden |
Not an administrator. |
409 |
not_draft |
Already sent or going out. |
422 |
incomplete |
Subject, text, category or included segments missing; context.missing lists them (subject, body, category, segments). |
422 |
no_connection |
The workspace has no sending connection. |
422 |
no_recipients |
Nobody left who may be mailed. |
422 |
audience_changed |
The audience is not count; context.count is the current number. |
422 |
confirm_required |
From the threshold on, typed has to equal the audience. |
Validation errors on count and typed are the usual 422 with errors.
Schedule a mailing
/api/v1/workspaces/{workspace}/notify/mailings/{mailing}/scheduleSets a draft (or an already scheduled mailing) to go out at at. It asks for the same confirmation as
sending, with count and typed, plus:
| Field | Type | Required | Description |
|---|---|---|---|
at |
string | yes | An ISO 8601 moment with its offset, e.g. 2026-11-02T09:00:00+01:00. At least a minute ahead and at most a year. |
Answers 200 with the mailing in status scheduled. The count you confirmed is an indication: the audience is worked
out again at the moment of sending, as the person who scheduled it, and it is not sent if they may no longer send.
Changing a scheduled mailing takes it back to draft. The errors are those of sending, plus 422 invalid_time when
at is too soon or too far ahead.
curl https://backlog.polder.work/api/v1/workspaces/7/notify/mailings/12/schedule \
-H "Authorization: Bearer $POLDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"count": 38, "at": "2026-11-02T09:00:00+01:00"}'