Auth
Every endpoint accepts the brand API key two ways:
- In the URL:
/api/public/contacts/<API_KEY> - Bearer header:
Authorization: Bearer <API_KEY>
Find the key under Brand settings → API. CORS is open — these endpoints can be called from a browser.
Contacts
Upsert
POST /api/public/contacts/<API_KEY>
{
"email": "[email protected]",
"first_name": "Alice",
"tags": ["signup", "free-trial"]
}Idempotent by email. Tags are additive — existing tags survive.
Update
PUT /api/public/contacts/<API_KEY>/update
{
"email": "[email protected]",
"status": "unsubscribed"
}Delete
DELETE /api/public/contacts/<API_KEY>/[email protected]Hard delete. Cascades to events, send history, list memberships.
Move between tags/lists
POST /api/public/contacts/<API_KEY>/move
{
"email": "[email protected]",
"add_to": ["paid"],
"remove_from": ["free-trial"]
}Transactional
POST /api/public/transactional/<API_KEY>/send
{
"template_id": "tpl_welcome",
"to": "[email protected]",
"variables": { "name": "Alice" }
}See Transactional API for full details.
Campaigns
Create, edit, send and track campaigns. These endpoints need the brand API key (ml_live_…, Settings → API). Transactional project keys (ml_tx_…) are rejected with 403 brand_key_required, so a key handed to one app can't email your whole audience.
List
GET /api/public/campaigns/<API_KEY>?limit=50&offset=0&search=&status=draftlimit is 1–200 (default 50). status is one of draft, scheduled, sending, warmup, sent or all. Each campaign comes with a stats object; HTML bodies are left out to keep the list small.
{
"campaigns": [
{
"id": "cmp_xxx",
"name": "November newsletter",
"subject": "What's new",
"status": "sent",
"target_tags": ["newsletter"],
"send_to_all": false,
"stats": {
"total": 1200, "sent": 1190, "failed": 10,
"unique_opens": 540, "unique_clicks": 120,
"opens": 810, "clicks": 165,
"bounces": 6, "complaints": 0, "unsubscribes": 3,
"open_rate": 0.4538, "click_rate": 0.1008, "bounce_rate": 0.005
}
}
],
"total": 1,
"limit": 50,
"offset": 0
}Rates use the delivered (sent) count as the denominator.
Create a draft
POST /api/public/campaigns/<API_KEY>
{
"name": "November newsletter",
"subject": "What's new, {{ first_name }}",
"preheader": "Three updates this month",
"from_email": "[email protected]",
"from_name": "Acme",
"reply_to": "[email protected]",
"html": "<h1>Hi {{ first_name }}</h1>…",
"text_override": null,
"target_tags": ["newsletter"],
"send_to_all": false
}Only name is required, but one call can build a draft that's ready to send. from_email and from_name default to the brand's. Pick the audience with target_tags (up to 50) or send_to_all: true. Returns 201 with the full campaign. Campaigns created through the API open in the HTML editor in the dashboard.
Get
GET /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>Returns the full campaign, including html and text_override.
Update
PUT /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>
{
"subject": "Updated subject",
"target_tags": ["newsletter", "customers"]
}Partial: send only the fields you want to change (PATCH works too). Same fields as create. Drafts and scheduled campaigns can be edited; once a campaign is sending, sent or in warmup you get 409 campaign_locked.
Delete
DELETE /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>Returns { "deleted": true, "id": "…" }. Refused with 409 campaign_sending while the campaign is sending.
Send now
POST /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>/send
{}Returns 202 with { "scheduled": false, "mode": "send_now", "audience_size": 1200 }. Optionally pass send_rate_per_sec (0.1–1000 emails/second) to pace the send; leave it out to use the brand default.
Schedule
POST /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>/send
{
"scheduled_at": 1767225600000
}scheduled_at is a Unix timestamp in milliseconds. It must be more than 60 seconds in the future, otherwise the campaign sends now. Returns 202 with mode: "schedule".
Warmup
POST /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>/send
{
"mode": "warmup",
"warmup": {
"initial_batch_size": 100,
"increment_factor": 1.5,
"increment_interval_h": 24,
"max_batch_size": 5000,
"start_at": 1767225600000
}
}Sends in growing batches: starts at initial_batch_size, multiplies by increment_factor (1.01–10) every increment_interval_h hours (0.5–168), capped at max_batch_size. Returns 202 with audience_size and total_stages.
Cancel a scheduled send or warmup
DELETE /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>/sendMoves the campaign back to draft. Recipients a warmup already reached keep their send history. Anything other than a scheduled or warmup campaign returns 409 not_cancellable.
Stats
GET /api/public/campaigns/<API_KEY>/<CAMPAIGN_ID>/stats{
"status": "sent",
"audience": 1200,
"sends": { "total": 1200, "pending": 0, "sent": 1190, "failed": 10, "skipped": 0 },
"events": {
"opens": 810, "unique_opens": 540,
"clicks": 165, "unique_clicks": 120,
"bounces": 6, "complaints": 0, "unsubscribes": 3
}
}Send errors
400 missing_fields: the campaign needs a subject, HTML, a from address, an audience and a configured provider. Thefieldsarray lists what's missing.400 empty_audience: no subscribed contacts match the tags.400 warmup_config_required,warmup_start_in_past,invalid_warmup_caps: warmup settings are missing or invalid.402 license_required: activate the install under Settings → License.409 already_sending/already_sent.
Sequences
POST /api/public/sequences/<API_KEY>/enroll
{
"sequence_id": "seq_xxx",
"email": "[email protected]"
}Manually enroll a contact. Returns 200 if enrolled, 409 if already in the sequence.
Forms
POST /api/public/forms/<slug>/submit
{
"email": "[email protected]",
"fields": { "company": "Acme", "size": "10-50" }
}No API key required — forms are public by design.
429 Too Many Requests with Retry-After. Campaign endpoints have their own limits: 300 req/min on reads, 60 req/min on writes, and 10 req/min on /send.