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, at Settings → API keys:
- Give the key a name (e.g. "n8n automation") and click Create key.
- The full key (
sk_live_…) is shown once. Copy it now — only a hash is stored, so it can't be shown again. - Paste it into your integration as the bearer token.
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:
- 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.