Getting Started with the AcelleMail REST API

AcelleMail ships a REST API that exposes nearly every operation the admin UI offers — create lists, add subscribers, send campaigns, fire automations, read reports. This guide walks the 4 things you need to make your first successful API call: where to find your token, how to send it, what to call first, and how to debug when it doesn't work.

What this is for

AcelleMail ships a REST API that exposes nearly every operation the admin UI offers — create lists, add subscribers, send campaigns, trigger automations, read reports. If you've ever wished you could "do that thing in the admin, but from my own code", the API is the answer.

This guide is the 5-minute intro: where to find your API token, two ways to send it, the first call to make to confirm everything works, and the common failure modes. Once this works, jump to the REST API Authentication and Endpoints reference for the full surface area.

Step 1 — Get your API token

Every AcelleMail account (admin OR customer) has an api_token stored in the database. Two ways to retrieve it:

Method A — From the UI (easiest)

  1. Log in to AcelleMail
  2. Click your name (top-right) → Account / Profile
  3. Look for the API Token field (or Developer / API section depending on your version)
  4. Click "Generate token" if no token exists, or "Show" / "Copy" if one does

The token is a long random string like Y2x4cTBxbGZ6MDAwMDAxazB0cWxqYmJ4dQ. Treat it like a password — anyone with this token can act as you.

Method B — Via the login endpoint (for headless / scripted setups)

curl -X POST https://mail.example.com/api/v1/user/login \
  -H "Accept: application/json" \
  -d 'email=you@example.com' \
  -d 'password=YourPassword'

# Response:
# {"api_token": "Y2x4cTBxbGZ6MDAwMDAxazB0cWxqYmJ4dQ"}

Store the returned api_token in your code's secrets manager / .env file. Never commit it to version control.

Step 2 — Two ways to send the token

Both are equivalent; pick whichever fits your HTTP client better.

Method A — Authorization: Bearer header (preferred)

curl -H "Authorization: Bearer Y2x4cTBxbGZ6MDAwMDAxazB0cWxqYmJ4dQ" \
     -H "Accept: application/json" \
     https://mail.example.com/api/v1/me

Standard OAuth-style header. Works in every HTTP client. Doesn't show up in server access logs (where the URL does).

Method B — ?api_token=XXX query parameter

curl "https://mail.example.com/api/v1/me?api_token=Y2x4cTBxbGZ6MDAwMDAxazB0cWxqYmJ4dQ" \
     -H "Accept: application/json"

Easier for quick CLI testing. Avoid in production — query strings get logged in access logs, browser history, referrers. Use the header.

Step 3 — Your first call

The /api/v1/me endpoint returns the authenticated user's profile. It's the standard "is my auth working?" check:

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     https://mail.example.com/api/v1/me

Expected response (HTTP 200):

{
  "id": 1,
  "uid": "Y2x4cTBxbGZ6MDAwMDAxazB0cWxqYmJ4dQ",
  "email": "you@example.com",
  "first_name": "Your",
  "last_name": "Name",
  "created_at": "2024-01-15T10:32:11.000000Z",
  "customer": {
    "id": 5,
    "uid": "...",
    "company": "Acme Corp"
  }
}

If you see your own data, the API is working and your auth is correct. Move on to the endpoint reference.

Step 4 — Client snippets

Once auth works in curl, the same pattern translates to any language. Here are tested snippets:

PHP (Guzzle)

$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://mail.example.com/api/v1/',
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('ACELLEMAIL_TOKEN'),
        'Accept' => 'application/json',
    ],
]);

$response = $client->get('me');
$me = json_decode($response->getBody(), true);
echo $me['email'];

Python (requests)

import os
import requests

BASE = 'https://mail.example.com/api/v1'
HEADERS = {
    'Authorization': f"Bearer {os.environ['ACELLEMAIL_TOKEN']}",
    'Accept': 'application/json',
}

r = requests.get(f'{BASE}/me', headers=HEADERS)
r.raise_for_status()
print(r.json()['email'])

Node.js (fetch)

const BASE = 'https://mail.example.com/api/v1';
const token = process.env.ACELLEMAIL_TOKEN;

const res = await fetch(`${BASE}/me`, {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Accept': 'application/json',
  },
});
const me = await res.json();
console.log(me.email);

Go (net/http)

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    req, _ := http.NewRequest("GET", "https://mail.example.com/api/v1/me", nil)
    req.Header.Set("Authorization", "Bearer "+os.Getenv("ACELLEMAIL_TOKEN"))
    req.Header.Set("Accept", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var me struct{ Email string }
    json.NewDecoder(resp.Body).Decode(&me)
    fmt.Println(me.Email)
}

Step 5 — Try something useful

Now that auth works, fetch your subscriber lists:

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Accept: application/json" \
     https://mail.example.com/api/v1/lists

Response shape:

{
  "data": [
    { "uid": "abc123", "name": "Newsletter", "subscribers_count": 5230, ... },
    { "uid": "def456", "name": "Customers", "subscribers_count": 1842, ... }
  ],
  "current_page": 1,
  "per_page": 20,
  "total": 2
}

The uid field is what you'll use for subsequent calls (e.g. GET /api/v1/lists/abc123/subscribers). See the endpoint reference for what each resource looks like.

Common issues

What you see Likely cause Fix
401 Unauthenticated Wrong / expired / missing token Re-fetch from UI; verify you're hitting the right base URL (/api/v1/ not /api/)
404 Not Found on a known endpoint Wrong base URL — used /api/me instead of /api/v1/me All endpoints are versioned under /v1/
415 Unsupported Media Type Missing Accept: application/json header Always include this header on every request
422 Unprocessable Entity Validation error on request body Response body lists exactly which fields failed; fix and retry
429 Too Many Requests Rate-limit hit (if your install has rate limiting) Back off; check Retry-After header for wait time
500 Internal Server Error Server-side bug Check /var/www/acellemail/storage/logs/laravel.log for the stack trace
Empty data: [] on /lists Token belongs to a different customer with no lists Verify which customer the token is for via /mecustomer.id; lists are scoped per customer
Different shape than docs say API response shape varies slightly across major Acelle versions Print the full JSON in development; rely on field presence checks
cURL: (60) SSL certificate problem Self-signed cert on staging install curl -k ... for testing; fix the cert before production
Login endpoint returns "Invalid credentials" Email/password wrong, OR 2FA enabled If 2FA is on, you can't get a token via login endpoint — use the UI method (Step 1A)

Rate limits

By default AcelleMail doesn't impose API rate limits — it trusts authenticated users. If you're hitting the API hard (1000+ calls/sec from a script), consider adding throttling middleware via the extending source code guide.

For public endpoints (signup forms via /api/v1/public/subscribers), DO add throttling — it's the typical spam-bot target.

What's authenticated as what

A token belongs to one user, and inherits that user's permissions:

  • Admin user token: can access /customers, /subscriptions, /sending_servers admin endpoints + all customer endpoints
  • Customer user token: can only access endpoints scoped to their customer record (their lists, their campaigns, their automations)

This matters for SaaS operators — never give an end customer an admin token; generate them their own.

Versioning

Current version is v1. AcelleMail has held this API stable across multiple major Acelle releases (4.x → 5.x). Breaking changes are rare; additive changes (new endpoints, new fields) are common — always parse responses defensively (response.field ?? default).

If a future /v2/ ships, the announcement will be in acellemail-updates with a migration period for v1.

FAQ

Can I use OAuth instead of token? Not natively in current AcelleMail. The token is functionally equivalent for machine-to-machine use cases.

Is there a Postman collection? Not officially shipped. Build your own by importing the OpenAPI spec — see the endpoint reference for the resource layout.

Can a token be revoked? Yes — generate a new one in the UI (it replaces the old one). The old token immediately becomes invalid.

How do I expire tokens after N days? Not built-in; add a custom Laravel scheduled task that nulls users.api_token for inactive accounts.

Webhooks? Yes, separate from the API — see Webhook Events Reference for outbound events (new_subscription, change_plan, etc.).

Is there a Python / JS SDK? Not officially. The API surface is small enough that a hand-rolled client per language is straightforward — see Step 4 snippets.

Can I use this from a browser (CORS)? AcelleMail doesn't ship CORS headers by default — your /api/v1/* calls from a different origin will be blocked. Either proxy through your own backend (recommended) or add CORS headers in nginx for the API routes (see post-install hardening).

Related articles

17 コメント

コメント 6 件

  1. bos.devops
    Webhook signature verification is the kind of thing ost docs hand-wave. The constant-time-compare warning is genuinely important — Ive seen production codebases with == on signatures.
  2. danrey.dev
    Can webhook secrets be rotated without downtime, or is there an overlap mechanism? We rotate everything quarterly.
    1. admin
      We don't recommend that approach in production. It works in dev but has subtle race conditions under concurrent load. Stick with the documented pattern
    2. admin (編集済み)
      Yes, that pattern is supported. The undocumented bit is the order — config:cache MUST come after the migration, not before. Updating the docs to make that explicit.
    3. admin (編集済み)
      There's no built-in way today. Two workarounds: (1) cron + custom script polling the API every N minutes, (2) webhook-driven if your event source supports it. Most operators go with #2
    4. admin (編集済み)
      currently a manual step. Theres a feature request tracking it on the repo if you want to +1.
    5. admin (編集済み)
      Honest answer: it depends on your provider. SES handles it gracefully; Mailgun is stricter. Well add a provider-by-provider table in the next revision.
  3. lequan.saigon
    If you're on Node, use the `crypto.timingSafeEqual()` instead of `===` for signature compare. The article mentions it but it's worth emphasizing — this is a real CVE pattern.
  4. aisha.khan.pak
    Built a Cloudflare Worker to bridge Shopify → AcelleMail webhooks last year. The signature verification pattern in this article is the same one I used. ~30 lines total...
    1. admin (編集済み)
      Thanks for sharing. The pattern you describe is exactly the use case we built that feature for — glad it landed for you
  5. tranminh.devop…
    Whats the recommended retry policy for failed deliveries on the receiver side? We see ~1% transient failures and aren't sure if we should auto-retry or rely on AcelleMail's redelivery
    1. admin
      Short answer: yes — set the MySQL session variable from your worker's .env on boot and you'll get the longer timeout per connection. We'll add an explicit recipe in the next refresh...
    2. admin (編集済み)
      suppression list import via csv captures all opt-outs including preference-center ones if you exported with the right field set. the export filter defaults exclude some — check the 'include unsubscribed' checkbox on mailchimp's export wizard...
    3. admin (編集済み)
      Good catch. The bounds (200/32) are hardcoded in the runtime. We've discussed making them configurable; not a near-term priority but it's tracked.
  6. cmendoza.mx
    webhook signature verification is the kind of thing most docs hand-wave. the constant-time-compare warning is genuinely important — i've seen production codebases with == on signatures.
    1. admin (編集済み)
      Thanks for the kind words. We try to keep these source-grounded so they age well.
    2. admin (編集済み)
      glad it landed. drop suggestions in the comments and well incorporate them on the next refresh.

More in Developer Guide