Skip to content
Mail Gazelle Mail Gazelle

Public API

How your app talks to Mail Gazelle

Send transactional email over HTTPS. Authenticate with a product token, post JSON, and Mail Gazelle queues the send. This page is the integration contract.

Set up a product first

Tokens are minted on a ready product. Ready means sending DKIM is verified and MAIL FROM succeeded. The raw token is shown once in the admin UI.

  1. 01

    Create a product with a name and primary domain. Sending identity and MAIL FROM default to notif.{primary} and bounce.notif.{primary}; both can be edited on create.

  2. 02

    Add the DNS records from the product checklist for that sending identity and MAIL FROM.

  3. 03

    Click Check now, or wait for the minute poll. The product becomes ready only when sending DKIM and MAIL FROM both succeed.

  4. 04

    Create an API token.

The apex domain is not created as an SES identity on the default path. DMARC is recommended. It is not a ready-gate.

A disabled or paused team, or a product that is not ready, cannot send. The API returns 403 with code product_not_ready.

Base URL and auth

Base URL
https://mailgazelle.com/api/v1
Header
Authorization: Bearer tes_…
Rate limit
60 requests per minute per token

Send an email

POST /emails

Persists the message, stores attachments on disk, and queues a send. Returns 202 immediately. SES is not called in this request.

{
  "id": "01J…",
  "status": "queued"
}
Field Required Notes
to Yes One or more { "email", "name?" }. At most 50 addresses including cc and bcc.
cc No Same shape as to.
bcc No Same shape as to. Delivered, but omitted from the visible message headers.
subject Yes Message subject.
html or text One required HTML is capped at 512 KB. Both may be sent together.
from No Defaults to the product From. Domain must be that product's primary or sending host.
reply_to No Defaults to the product Reply-To. Send [] to omit it.
headers No Header name to string. From, To, Cc, Bcc, Reply-To, Sender, Subject, Content-Type, and Return-Path are ignored. Message-ID is kept.
tags No Object of names and values matching [A-Za-z0-9_-]. Invalid tags are rejected, not dropped. At most 48.
idempotency_key No Unique per team. The same key returns the original message and does not send again. A replay is not checked against the monthly quota.
attachments No JSON array. Same shape as Resend / Postmark.

Check a message

GET /emails/{id}

Returns status, timestamps, the SES message id, last event type, and attachment metadata (filename, content type, size). It does not return HTML, text, or file bytes.

List domains

GET /domains

Lists the token's product domains and their verification statuses.

Attachments

Optional JSON array. Same shape as Resend and Postmark.

"attachments": [
  {
    "filename": "invoice.pdf",
    "content": "<base64>",
    "content_type": "application/pdf",
    "content_id": null
  }
]
  • filename and content are required.
  • content_type is optional. Mail Gazelle guesses it from the filename.
  • content_id is optional. When set, the file is embedded (reference it as cid:… in the HTML). Empty or duplicate ids are attachment_invalid.
  • Filename must be a basename. Path segments are rejected ( attachment_invalid).
  • Your plan sets the attachment count and decoded total per message (by default 10 attachments and 7 MB, which is also the platform ceiling). Too many is validation_error; too large is attachment_too_large. The monthly attachment allowance is separate (quota_exceeded). The assembled message, including encoding, must be 10 MB or smaller (message_too_large).
  • Empty or invalid base64 is attachment_invalid.

SES Simple cannot carry attachments. Mail Gazelle always sends Content.Raw.

Suppressions

The send path checks the team (account) list and the global suppression list, for every to, cc, and bcc address. A suppressed recipient is stored as a rejected message. Mail Gazelle does not call SES. The API returns 422:

{
  "message": "This recipient is suppressed.",
  "code": "recipient_suppressed"
}

Permanent bounces and complaints write the address to the team list and the SES account suppression list. A suppressed reject does not consume the monthly email or attachment quota.

Quotas

Your team's plan sets the emails and decoded attachment size included each UTC month, plus an optional daily send limit (UTC day).

When a new send would pass a limit, the API returns 422 quota_exceeded and does not queue the message. message says which limit was hit: monthly email quota, daily email limit, or monthly attachment quota. Plans that allow additional sending keep sending past the monthly amounts; the daily limit still applies. Each distinct to, cc, and bcc address counts as one send, and decoded attachment bytes count once per recipient. The same address listed more than once counts once. recipient_suppressed rejects are not counted. Replaying a stored idempotency key does not re-check any limit. A send with no attachments is not blocked by the attachment allowance.

Errors

All API errors use this shape. details is optional. Responses never include AWS stack traces, message bodies, or attachment bytes.

{
  "message": "…",
  "code": "…",
  "details": {}
}
Code Typical status
unauthenticated 401
product_not_ready 403
validation_error 422
from_not_allowed 422
recipient_suppressed 422
attachment_invalid 422
attachment_too_large 422
quota_exceeded 422
html_too_large 422
message_too_large 422
rate_limited 429

Sandbox vs production

Until AWS takes the SES account out of sandbox, you can only send to verified identities. Simulator addresses work in sandbox:

Examples

Text send

curl -X POST "https://mailgazelle.com/api/v1/emails" \
  -H "Authorization: Bearer tes_…" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{"email": "[email protected]"}],
    "subject": "Welcome",
    "text": "Thanks for signing up."
  }'

Send with a PDF

curl -X POST "https://mailgazelle.com/api/v1/emails" \
  -H "Authorization: Bearer tes_…" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{"email": "[email protected]"}],
    "subject": "Invoice",
    "html": "<p>Your invoice is attached.</p>",
    "attachments": [{
      "filename": "invoice.pdf",
      "content": "'$PDF_BASE64'",
      "content_type": "application/pdf"
    }]
  }'