Public API

HTTP endpoints for contacts, transactional sends, campaigns, sequences, and forms.

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=draft

limit 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>/send

Moves 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. The fields array 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.
Sending can't be undone
A send-now call starts emailing your audience straight away. Schedule it if you want a window to cancel.

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.

Rate limits
Per-brand: 100 req/min on writes, 600 req/min on reads. Trips return 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.