SENDMAILER.CC

API Documentation

Production REST API for multi-brand transactional and marketing email on AWS SES. Suppression, bounce/complaint handling, open/click tracking and signed outbound webhooks are applied automatically.

Base URL & authentication

All requests are HTTPS and authenticated with your company's API key as a Bearer token. Your key looks like smk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and is shown once when your account is provisioned — store it as a secret. The company is resolved from the key; never send a company id in the body.

Base URL: https://api.sendmailer.cc

Authorization: Bearer smk_your_key_here
Content-Type: application/json
Accept: application/json

Quick start — Laravel app

Two integration paths for Laravel:

1. Consuming app .env

# SENDMAILER.CC HTTP API
SENDMAILER_BASE_URL=https://api.sendmailer.cc
SENDMAILER_KEY=smk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Default From identity — must match an authorised sender configured for your company
[email protected]
MAIL_FROM_NAME="Your Company"

Note: the consuming app's MAIL_FROM_ADDRESS is informational — SENDMAILER.CC uses the company's default From, configured by the platform admin. When sender-identity enforcement is on, the local-part is checked against an allowlist.

2. config/services.php

'sendmailer' => [
    'key'      => env('SENDMAILER_KEY'),
    'base_url' => env('SENDMAILER_BASE_URL', 'https://api.sendmailer.cc'),
],

3. Send

use Illuminate\Support\Facades\Http;

Http::withToken(config('services.sendmailer.key'))
    ->acceptJson()
    ->post(config('services.sendmailer.base_url') . '/api/v1/email/send', [
        'to'       => ['email' => $user->email, 'name' => $user->name],
        'template' => 'order-confirmation',
        'data'     => [
            'first_name' => $user->first_name,
            'order_id'   => $order->id,
            'total'      => $order->total,
        ],
    ])
    ->throw();

Two one-time setup items before going live:

v3 REST API

The full v3 REST surface lives under https://api.sendmailer.cc/v3/ and covers transactional sends, templates, sender identities, suppression, marketing contacts & lists, single sends, stats and webhook subscriptions. Authenticate with your Bearer key:

Http::withToken(env('SENDMAILER_KEY'))
    ->acceptJson()
    ->post('https://api.sendmailer.cc/v3/mail/send', $payload);
GroupEndpoints
SendPOST /v3/mail/send — accepts personalizations[], template_id, content[], categories, custom_args; returns 202 + X-Message-Id
TemplatesGET POST /v3/templates, GET PATCH DELETE /v3/templates/{id}
SendersGET POST /v3/senders, GET PATCH DELETE /v3/senders/{id}
SuppressionGET DELETE /v3/suppression/bounces[/{email}], GET DELETE /v3/suppression/unsubscribes[/{email}]
Marketing contactsGET PUT DELETE /v3/marketing/contacts[/{id}] (PUT = batch upsert by email)
Marketing listsGET POST PATCH DELETE /v3/marketing/lists[/{id}] + POST DELETE /v3/marketing/lists/{id}/contacts
Single sendsGET POST DELETE /v3/marketing/singlesends[/{id}] + lifecycle actions /now, /schedule, /pause, /resume, /cancel, /test
StatsGET /v3/stats?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD

API conventions: responses are unwrapped JSON (no {"data": ...} envelope); bounce/suppression deletes are idempotent (204 whether or not the row existed); rate limits + per-key IP allowlist + sender-identity enforcement all apply to /v3 the same as to the native /api/v1 surface.

Supported /v3/mail/send fields

Payload fieldBehaviour
personalizations[].to/cc/bcc/subjectPer-recipient fan-out; CC + BCC delivered; per-personalization subject overrides top-level
personalizations[].dynamic_template_dataBound to template {{ placeholders }} when template_id is set
reply_to (top or per-personalization)Per-send Reply-To header; personalization overrides top-level
from {email,name}Per-send From; still gated by sender-identity enforcement when on
content[] {type, value}Raw HTML/text body — alternative to template_id
template_idSlug of a template you created on this account
categories, custom_argsPersisted on each message; surfaced on events
headersArbitrary custom headers; reserved headers (From/To/Subject/etc.) and CRLF-injection attempts are filtered
attachments[]Per-send attachments (10 MB per file, 25 MB aggregate, MIME-allowlisted)
mail_settings.sandbox_mode.enable: trueValidates + persists an audit row, does not actually send. Returns 202 + X-Message-Id
Idempotency-Key request headerReplays with same body return the cached 2xx + Idempotent-Replayed: true; replays with a different body return 422

Response envelope: 202 Accepted with X-Message-Id (first), X-Message-Ids (CSV), X-Message-Count. When your key has a per-minute rate limit, every authenticated response — including 429s — also carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

Idempotency keys

Send the optional Idempotency-Key request header (any string, typically a UUID or business identifier) and we'll cache the 2xx response for 24 hours. Replays with the same body return the cached response plus an extra Idempotent-Replayed: true header. Replays with the same key but a different body return 422 — the contract protects you from a retry that accidentally double-sends:

curl -X POST https://api.sendmailer.cc/v3/mail/send \
  -H "Authorization: Bearer $SENDMAILER_KEY" \
  -H "Idempotency-Key: order-abc-123-confirmation" \
  -H "Content-Type: application/json" \
  -d '{ "personalizations":[…], "from":{…}, "content":[…] }'

Attachments

Each entry is base64-encoded content with a MIME type and filename. Per-file cap 10 MB, total cap 25 MB, MIME allowlist (executables like application/x-msdownload, .exe, .msi, .sh, .php are rejected with a clear 422). Inline images carry a content_id:

{
  "attachments": [
    {
      "content":  "JVBERi0xLjQKJ…",
      "type":     "application/pdf",
      "filename": "invoice.pdf"
    },
    {
      "content":     "iVBORw0KGgo…",
      "type":        "image/png",
      "filename":    "logo.png",
      "disposition": "inline",
      "content_id":  "logo"
    }
  ]
}

Sandbox mode — test without sending

Pass mail_settings.sandbox_mode.enable: true and we validate + persist the message but do not actually deliver. Same 202 + X-Message-Id envelope as a real send; the persisted row is marked status: sandboxed so you can see it on the activity page. Useful for staging environments, CI smoke tests, and rate-limit experiments:

{
  "personalizations":[…],
  "from":{…},
  "content":[…],
  "mail_settings": { "sandbox_mode": { "enable": true } }
}

Bulk send — Single Sends lifecycle

For sending a single template to a contact list (newsletter blast, announcement), use the Single Sends surface:

# 1. create the single send (status: draft)
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Q3 newsletter","template_id":"q3-news","list_id":42}'

# 2. start it immediately…
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/now \
  -H "Authorization: Bearer $SENDMAILER_KEY"

# …or schedule it
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/schedule \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{"send_at":"2026-06-01T10:00:00Z"}'

# pause / resume / cancel mid-flight
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/pause   …
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/resume  …
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/cancel  …

# preview to your own inbox first
curl -X POST https://api.sendmailer.cc/v3/marketing/singlesends/{id}/test \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'

State machine: draftscheduled / sendingpausedsendingcompleted / cancelled. Invalid transitions return 422 with a clear reason; recipient counts + sent/skipped/failed are visible on GET /v3/marketing/singlesends/{id}.

Marketing — contacts & lists

Batch-upsert contacts (insert-or-update by email) and manage lists:

# batch upsert (up to 30,000 per call)
curl -X PUT https://api.sendmailer.cc/v3/marketing/contacts \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{
    "list_ids": [42],
    "contacts": [
      {"email":"[email protected]","first_name":"Jane","custom_fields":{"plan":"growth"}},
      {"email":"[email protected]","first_name":"Bob"}
    ]
  }'
# → 202 { "job_id":"…", "created_count":1, "updated_count":1 }

# create a list
curl -X POST https://api.sendmailer.cc/v3/marketing/lists \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{"name":"VIPs","description":"Top-100 by LTV"}'

# attach existing contacts
curl -X POST https://api.sendmailer.cc/v3/marketing/lists/42/contacts \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Content-Type: application/json" \
  -d '{"contact_ids":[1,2,3]}'

Stats GET /v3/stats

Daily aggregates of sends + events over a window. Returns one row per day, with a metrics object holding the message-status counts and event counts:

GET https://api.sendmailer.cc/v3/stats?start_date=2026-05-01&end_date=2026-05-20

{
  "start_date": "2026-05-01",
  "end_date":   "2026-05-20",
  "stats": [
    { "date": "2026-05-19", "metrics": { "sent": 1240, "delivered": 1220, "bounce": 8, "open": 612, "click": 187 } },
    { "date": "2026-05-20", "metrics": { "sent":  980, "delivered":  974, "bounce": 3, "open": 503, "click": 142 } }
  ]
}

Outbound event webhooks

Subscribe a URL of yours to receive bounce / delivered / spamreport / unsubscribe / open / click events. Path: /v3/user/webhooks/event/settings.

curl -X POST https://api.sendmailer.cc/v3/user/webhooks/event/settings \
  -H "Authorization: Bearer $SENDMAILER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-events",
    "url": "https://your-app.example/sendmailer/events",
    "event_types": ["bounce","delivered","spamreport","unsubscribe"]
  }'
# → 201 with {subscription, signing_secret, message}

Store the signing_secret at creation time — it is shown only once. Rotate via POST /v3/user/webhooks/event/settings/{id}/rotate; the old secret is invalidated immediately.

Headers on every delivery

HeaderMeaning
X-Sendmailer-Signaturehex(hmac_sha256(body, signing_secret))
X-Sendmailer-Timestampunix epoch — reject if older than ~5 min
X-Sendmailer-Event-Iduuid — use as receiver-side idempotency key

Verifying on the receiver (Laravel)

Route::post('/sendmailer/events', function (Request $request) {
    $body = $request->getContent();
    $expected = hash_hmac('sha256', $body, config('services.sendmailer.webhook_secret'));
    if (! hash_equals($expected, $request->header('X-Sendmailer-Signature', ''))) {
        abort(401);
    }
    if (abs(now()->timestamp - (int) $request->header('X-Sendmailer-Timestamp', 0)) > 300) {
        abort(401);
    }
    // process $request->json()
    return response()->noContent();
})->withoutMiddleware([VerifyCsrfToken::class]);

Reliability

Laravel drop-in — sendmailer/laravel

Already on Laravel Mailables? Install the companion package and your Mail::to(...)->send(new Mailable) code keeps working — no rewrites:

composer require sendmailer/laravel
# .env
MAIL_MAILER=sendmailer
SENDMAILER_KEY=smk_xxxxx
SENDMAILER_BASE_URL=https://api.sendmailer.cc
// config/mail.php
'mailers' => [
    'sendmailer' => ['transport' => 'sendmailer'],
    // …
],

Attachments, CC, BCC, Reply-To, inline images (content_id) and custom X-* headers all pass through to /v3/mail/send unchanged. SentMessage::getMessageId() returns sendmailer's X-Message-Id.

Native API — Health check GET /api/v1/ping

curl https://api.sendmailer.cc/api/v1/ping \
  -H "Authorization: Bearer $SENDMAILER_KEY" -H "Accept: application/json"

{ "ok": true, "company": "your-company" }

A 401 means the key is missing, invalid, revoked, or the company is not active.

Send an email POST /api/v1/email/send

{
  "to":   { "email": "[email protected]", "name": "Jane Doe" },
  "template": "order-confirmation",
  "data": { "first_name": "Jane", "order_id": "ORD-1234", "total": "$59.00" }
}
FieldRequiredDescription
to.emailyesRecipient address
to.namenoRecipient display name
templateyesTemplate slug (managed per company in the admin)
datanoValues substituted into template placeholders

Responses

HTTPBodyMeaning
202{"id":123,"status":"queued"}Accepted & queued for delivery
200{"id":123,"status":"suppressed"}Recipient suppressed (unsubscribed / bounced / complained). Not sent — expected, not an error.
422{"message":"Unknown or inactive template..."}Template not found/inactive, or validation failed
401{"message":"..."}Missing/invalid/revoked key, or company inactive
429{"message":"Rate limit exceeded."}Per-key rate limit hit

Treat 202 and 200 as success. Retry only on 5xx / network errors using the same payload.

Example — cURL

curl -X POST https://api.sendmailer.cc/api/v1/email/send \
  -H "Authorization: Bearer $SENDMAILER_KEY" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"to":{"email":"[email protected]","name":"Jane"},
       "template":"order-confirmation",
       "data":{"first_name":"Jane","order_id":"ORD-1234"}}'

Example — Laravel

use Illuminate\Support\Facades\Http;

Http::withToken(config('services.sendmailer.key'))
    ->acceptJson()
    ->post('https://api.sendmailer.cc/api/v1/email/send', [
        'to'       => ['email' => $user->email, 'name' => $user->name],
        'template' => 'order-confirmation',
        'data'     => ['first_name' => $user->first_name, 'order_id' => $order->id],
    ])
    ->throw();

Templates

Templates are created per company in the admin panel. Each has a slug (used as template), a subject and HTML/text bodies. Placeholders use {{ key }} and are filled from data:

Authorised From-addresses

A verified sending domain lets your company send from some address on that domain. Your account admin may further restrict you to specific local-parts (e.g. no-reply, support, bookings) — a "Single Sender" allowlist tied to your company. You don't authorise these yourself; it's an admin-side setting on this account.

Suppression & behaviour

Need an API key? Get started.