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/v1

The 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:

  1. Give the key a name (e.g. "n8n automation") and click Create key.
  2. The full key (sk_live_…) is shown once. Copy it now — only a hash is stored, so it can't be shown again.
  3. 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 manager can't reach admin-only endpoints (team management); a key owned by a view_only user can read but every write returns 403.
  • 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, never 403 — 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]
  }'
MethodUsed forSuccess status
GETRead a resource or list200
POSTCreate, or run an action (activate, archive, enrol…)201 create · 200 action
PATCHPartial update200
DELETESoft-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:

ParamDefaultNotes
limit501–200
offset0Skip 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"
  }
}
StatuscodeMeaning
400invalid_requestMissing/invalid field, bad query param
401unauthorizedMissing, malformed, or revoked API key
403forbiddenKey's role isn't allowed to do this (e.g. view-only writing, non-admin hitting a team endpoint)
404not_foundNo such resource, or the key's owner can't see it
409conflictDuplicate — e.g. a contact with that email already exists
500internal_errorSomething 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's starts_at, an event feed's since) must be RFC 3339, e.g. 2026-09-10T14:00:00Z.
  • Deletes are soft. A DELETE hides the resource everywhere and it stops functioning, but the row is retained and an admin can restore it. The same is true of archive.
  • There is no rate limit today. Be reasonable; a tight polling loop should sleep between passes.

Full endpoint reference →