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.
-
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.
-
02
Add the DNS records from the product checklist for that sending identity and MAIL FROM.
-
03
Click Check now, or wait for the minute poll. The product becomes ready only when sending DKIM and MAIL FROM both succeed.
-
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
}
]
-
filenameandcontentare required. -
content_typeis optional. Mail Gazelle guesses it from the filename. -
content_idis optional. When set, the file is embedded (reference it ascid:…in the HTML). Empty or duplicate ids areattachment_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 isattachment_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"
}]
}'