REST API
Everything you can do in the SubsiMail browser UI — create campaigns, manage contacts and lists, enrol people, read stats and events, invite teammates — you can also do over a JSON REST API. It's designed to be driven from n8n, Make, Zapier (via an HTTP node), a cron job, or your own backend.
The API is a companion to webhooks: webhooks push events out to you as they happen; the API is how you pull data and push changes in.
Base URL & versioning
All endpoints live under /api/v1 on your own SubsiMail instance:
https://mail.yourcompany.com/api/v1The v1 in the path is the API version. Breaking changes will ship as v2; v1 keeps working. All requests and responses are JSON (Content-Type: application/json).
Authentication
Every request must carry a personal API key as a bearer token:
curl https://mail.yourcompany.com/api/v1/me \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
A request with no key, a revoked key, or a malformed key gets 401 Unauthorized. There are no cookies, sessions, or CSRF tokens involved — the key is the whole story.
GET /api/v1/me is the simplest way to check a key works. It returns the owning user and the key id:
{
"user_id": 4,
"name": "Priya Shah",
"email": "priya@yourcompany.com",
"role": "admin",
"api_key_id": 2
}Creating & managing keys
Keys are created from the browser, per user. In your SubsiMail instance:
- Open Settings in the sidebar, then the API keys card (direct URL:
/settings/api-keys). - Give the key a name (e.g. "n8n automation") and click Create key.
- The full key (
sk_live_…) is shown once, right after creation. Copy it now — only a hash is stored, so it can't be shown again. (Reloading the page won't bring it back, and won't create a second key.) - Paste it into your integration as the bearer token.
Workspaces: a key belongs to the workspace that was active when you created it, and acts only inside that workspace — its contacts, lists, campaigns, mailboxes and everything else are that workspace's alone. To automate another client, switch to its workspace and create a key there. Keys created before 8.0 belong to the Default workspace.
Keys don't expire. Revoke one anytime from the same page — any integration using it stops working on the next request. Create a separate key per integration so you can revoke them independently. If a key leaks, revoke it and issue a new one; nothing else is affected.
What a key can do
A key acts as the user who created it, with exactly the same permissions that user has in the UI:
- Workspace — the key sees and changes only its own workspace. Anything in another workspace returns
404, and team endpoints list and invite people for that workspace only. Your role is the one you hold in that workspace. - Role — a key owned by a
managercan't reach admin-only endpoints (team management); a key owned by aview_onlyuser can read but every write returns403. - Sharing / visibility — if a campaign or contact has been scoped to specific people, a key only sees it when its owner is one of them (or is an admin). A resource you can't see returns
404, never403— the API never confirms that something exists but is off-limits. - Audit trail — anything the key creates or changes is recorded against its owner, along with the request's IP address, and shows in the "history" popover in the UI just like a manual change.
Making requests
Send a JSON body on POST and PATCH. PATCH is a partial update — include only the fields you want to change.
curl -X POST https://mail.yourcompany.com/api/v1/contacts \
-H "Authorization: Bearer $SUBSIMAIL_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"email": "jane@acme.com",
"company": "Acme",
"custom_fields": { "plan": "pro" },
"list_ids": [3]
}'
| Method | Used for | Success status |
|---|---|---|
GET | Read a resource or list | 200 |
POST | Create, or run an action (activate, archive, enrol…) | 201 create · 200 action |
PATCH | Partial update | 200 |
DELETE | Soft-delete (recoverable by an admin) | 204 (no body) |
Lists & pagination
List endpoints return an object with data and pagination:
{
"data": [ { "id": 42, "name": "Q3 outbound", ... }, ... ],
"pagination": { "total": 137, "limit": 50, "offset": 0 }
}Control the window with query parameters:
| Param | Default | Notes |
|---|---|---|
limit | 50 | 1–200 |
offset | 0 | Skip this many rows |
To page through everything, keep requesting with offset += limit until offset >= pagination.total. Small collections that are never large (a campaign's steps, a resource's share list, custom fields) skip pagination and just return { "data": [ ... ] }.
Most list endpoints also accept filters — ?archived=true, ?status=active, ?q=jane, ?campaign_id=42. These are documented per endpoint in the reference.
Errors
Every 4xx/5xx response has the same shape:
{
"error": {
"code": "invalid_request",
"message": "account_id does not refer to a live account"
}
}| Status | code | Meaning |
|---|---|---|
400 | invalid_request | Missing/invalid field, bad query param |
401 | unauthorized | Missing, malformed, or revoked API key |
403 | forbidden | Key's role isn't allowed to do this (e.g. view-only writing, non-admin hitting a team endpoint) |
404 | not_found | No such resource, or the key's owner can't see it |
409 | conflict | Duplicate — e.g. a contact with that email already exists |
500 | internal_error | Something broke on the server; safe to retry |
Timestamps, IDs & conventions
- IDs are integers, unique per resource type, stable forever.
- Timestamps in responses are UTC, formatted
YYYY-MM-DD HH:MM:SS. Timestamps you send (e.g. a campaign'sstarts_at, an event feed'ssince) must be RFC 3339, e.g.2026-09-10T14:00:00Z. - Deletes are soft. A
DELETEhides the resource everywhere and it stops functioning, but the row is retained and an admin can restore it. The same is true ofarchive. - There is no rate limit today. Be reasonable; a tight polling loop should sleep between passes.