| Topic | What it covers |
|---|---|
| Audience | Contacts and tags |
| Content & Send | Templates, rendering, campaigns and sending |
| Recipient links | The token URLs that live inside sent emails |
Authentication
Every/api/v1 request carries an API key in the x-api-key header. A key is created from the app
and belongs to one organization: that organization is the entire scope of the key.
curl -H "x-api-key: $MK_API_KEY" \
"https://app.example.com/api/v1/tags"The key acts as the person who created it
A key is not a second identity with its own powers. It carries the rights of the member who created it, in the organization it was created for. A key made by anadmin can write; a key
made by a member can read. Nothing is granted through the API that the same person could not do
in the interface.
A key stops working when its owner leaves the organization. No orphan access survives a
departure: the key answers
401 from that moment, exactly as if it had been revoked. Revoking the
key of someone who left is therefore a cleanup, not a security fix.| Question | Decided by | Answer when it fails |
|---|---|---|
| Is the caller still a member, with which role? | The key's owner and their role | 401 / 403 |
| Does the resource belong to the key's organization? | The key's organization | 404 |
Who can do what
| Operation | Minimum role in the organization |
|---|---|
Reading anything, and POST /api/v1/render | member |
| Creating, updating, deleting, tagging, sending | admin |
POST /api/v1/render is the exception among the write verbs because it writes nothing: it renders
and returns, with no side effect.
Response envelope
| Case | Body |
|---|---|
| Success | {"success": true, "data": …} |
| Paginated list | {"success": true, "data": [...], "pagination": {"total", "page", "limit", "totalPages"}} |
| Error | {"error": "…"} |
| Validation error | {"error": "…", "details": [ … Zod issues … ]} |
| Status | When |
|---|---|
200 | Read or mutation done |
201 | Resource created |
202 | Send accepted, happening in the background — nothing is finished yet |
400 | Invalid parameters or body — details lists the offending fields |
401 | Missing or invalid key, or a key whose owner left the organization |
403 | The caller is a legitimate member, but their role is too low |
404 | Unknown resource, or a resource of another organization |
409 | State conflict — a tag name already taken, a campaign already sent |
429 | Rate limit of the key exceeded |
500 | Unexpected server error |
A resource of another organization answers
404, never 403. A 403 would confirm that the
id exists. Same response for an unknown id and for an id you are not allowed to read — you cannot
tell them apart, and that is the point. The 403 is reserved for the other question: you are in
the right organization, your role is too low.Every endpoint
Audience — details
| Endpoint | What it does |
|---|---|
GET /api/v1/contacts | Contacts of a list, paginated and filterable |
GET /api/v1/contacts/{contactId} | One contact with its tags |
PATCH /api/v1/contacts/{contactId} | Updates first name, last name or status |
POST /api/v1/lists/{listId}/contacts | Adds a contact to a list (upsert) |
POST /api/v1/lists/{listId}/contacts/bulk | Pushes up to 500 contacts at once, line by line |
POST /api/v1/contacts/{contactId}/unsubscribe | Unsubscribes a contact |
POST /api/v1/contacts/{contactId}/resubscribe | Puts a contact back to subscribed |
GET /api/v1/tags | Tags of the organization with their carrier count |
POST /api/v1/tags | Creates a tag |
POST /api/v1/contacts/{contactId}/tags | Puts a tag on a contact, by name (get-or-create) |
DELETE /api/v1/contacts/{contactId}/tags/{tagId} | Removes a tag from a contact |
Content & Send — details
| Endpoint | What it does |
|---|---|
GET /api/v1/templates | Templates of the organization, paginated |
GET /api/v1/templates/{templateId} | One template with its HTML |
POST /api/v1/templates | Creates a template |
PATCH /api/v1/templates/{templateId} | Updates name, subject or HTML |
DELETE /api/v1/templates/{templateId} | Deletes a template |
POST /api/v1/render | Renders HTML without sending anything |
GET /api/v1/campaigns | Campaigns of the organization, paginated |
POST /api/v1/campaigns | Creates a campaign in draft |
GET /api/v1/campaigns/{campaignId} | State, target, recipient count and send progress |
POST /api/v1/campaigns/{campaignId}/send | Triggers the send — answers 202, never waits |
Rate limit
The limit is carried by the key, not by the IP. A key over its limit answers429 on every
endpoint. Back off and retry: nothing was done.