AxeneAxene Docs
Sending Emails

Send via REST API

Send transactional and marketing emails programmatically using the Axene Mailer REST API.

The REST API is the most flexible way to send email through Axene Mailer. All requests go to https://mail.axene.io/v1/ and require an API key.

Authentication

Include your API key in the Authorization header on every request. API keys are prefixed with axm_k_:

Authorization: Bearer axm_k_YOUR_API_KEY

Generate API keys at Settings -> API Keys -> Create Key.

POST /v1/emails/

Send a single email. Note the trailing slash on the path.

Request body

FieldTypeRequiredDescription
from_objectYesSender address. Must be a registered sender on a verified domain.
from_.emailstringYesSender email address.
from_.namestringNoSender display name.
toarrayYesArray of recipient {email, name} objects. At least one required.
to[].emailstringYesRecipient email address.
to[].namestringNoRecipient display name.
subjectstringYesEmail subject line.
htmlstringNoHTML body. At least one of html or text is required.
textstringNoPlain text body. At least one of html or text is required.
ccarrayNoArray of {email, name} objects for CC recipients.
bccarrayNoArray of {email, name} objects for BCC recipients.
reply_toobjectNo{email, name} object for the Reply-To header.
headersobjectNoKey-value pairs of custom email headers.
tagsarrayNoArray of strings for categorization and filtering.
send_atstringNoISO 8601 datetime to schedule delivery. Omit to send immediately. See Scheduling.
attachmentsarrayNoArray of attachment objects (see below).

Attachments

Each attachment is an object with the file body base64-encoded:

FieldTypeRequiredDescription
filenamestringYesFile name. Must not contain path separators (/, \) or NUL.
content_base64stringYesFile body as base64. No data: URL prefix.
content_typestringNoMIME type. Defaults to application/octet-stream.

Each file is capped at 24 MiB, and the combined size of all attachments is also capped at 24 MiB. Oversized attachments return 413.

Example: cURL

curl -X POST https://mail.axene.io/v1/emails/ \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_": {
      "email": "[email protected]",
      "name": "Your Company"
    },
    "to": [
      {
        "email": "[email protected]",
        "name": "Jane Doe"
      }
    ],
    "subject": "Your order has shipped",
    "html": "<h1>Order Shipped</h1><p>Your package is on the way.</p>",
    "text": "Order Shipped\n\nYour package is on the way.",
    "tags": ["transactional", "order-updates"]
  }'

Example: Node.js (fetch)

const response = await fetch("https://mail.axene.io/v1/emails/", {
  method: "POST",
  headers: {
    "Authorization": "Bearer axm_k_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from_: {
      email: "[email protected]",
      name: "Your Company",
    },
    to: [{ email: "[email protected]", name: "Jane Doe" }],
    subject: "Your order has shipped",
    html: "<h1>Order Shipped</h1><p>Your package is on the way.</p>",
    text: "Order Shipped\n\nYour package is on the way.",
    tags: ["transactional", "order-updates"],
  }),
});
 
const data = await response.json();
console.log(data.id); // email ID

Example: Python (requests)

import requests
 
response = requests.post(
    "https://mail.axene.io/v1/emails/",
    headers={"Authorization": "Bearer axm_k_YOUR_API_KEY"},
    json={
        "from_": {
            "email": "[email protected]",
            "name": "Your Company",
        },
        "to": [{"email": "[email protected]", "name": "Jane Doe"}],
        "subject": "Your order has shipped",
        "html": "<h1>Order Shipped</h1><p>Your package is on the way.</p>",
        "text": "Order Shipped\n\nYour package is on the way.",
        "tags": ["transactional", "order-updates"],
    },
)
 
data = response.json()
print(data["id"])

Response

A successful request returns 202 Accepted:

{
  "id": "3f8c1e2a-9b4d-4c7e-8a1f-2d6b5e9c0a3d",
  "status": "queued",
  "message_id": "<[email protected]>",
  "rejection_reason": null
}
FieldDescription
idThe email's UUID. Use this to query status, events, or retry.
statusqueued for an immediate send, scheduled if send_at was provided.
message_idRFC 5322 Message-ID header value.
rejection_reasonReason the email was rejected, or null if accepted.

A 202 means the email was accepted for delivery, not that it was delivered. Use webhooks or the events endpoint to track delivery status.

Sending to a suppressed recipient

If any to, cc, or bcc address is on your suppression list, the send is rejected immediately, in this same 202 response, instead of being silently dropped later:

{
  "id": "3f8c1e2a-9b4d-4c7e-8a1f-2d6b5e9c0a3d",
  "status": "rejected",
  "message_id": null,
  "rejection_reason": "Recipient '[email protected]' is on your suppression list (reason: bounce, added 2026-08-01). This is a safety block, not necessarily permanent: send to this address again within 24 hours to deliver this one message as an override. It will be re-suppressed immediately after."
}

Addresses land on the suppression list automatically (hard bounces, unsubscribes) as well as manually, and sometimes that happens by mistake. Rather than requiring a dashboard trip to remove the suppression, a single deliberate retry within 24 hours is treated as an override:

  1. First attempt -> rejected, with the reason above. Nothing is sent.
  2. Retry within 24 hours, using POST /v1/emails/:id/retry on the rejected email (or by calling POST /v1/emails/ again with the same recipient) -> delivered once.
  3. Every attempt after that -> rejected again. The override is spent; the address stays suppressed until it's explicitly removed from the suppression list.

If you don't retry within the 24-hour window, the next attempt is treated as a fresh first attempt (rejected again, starting a new 24-hour window) rather than leaving an indefinitely pending override.

The override is a safety net for accidental suppressions, not a way to force mail to a genuinely bad address. Each suppressed address gets exactly one override; use it deliberately.

POST /v1/emails/validate

Dry-run validation. Checks whether an email would send without actually sending it. It validates the sender address, domain verification, plan send limits, recipients, content, and account restrictions. The request body is identical to POST /v1/emails/.

curl -X POST https://mail.axene.io/v1/emails/validate \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_": {"email": "[email protected]"},
    "to": [{"email": "[email protected]"}],
    "subject": "Test",
    "html": "<p>Hello</p>"
  }'

Response:

{
  "valid": true,
  "can_send": true,
  "issues": [],
  "plan": "starter",
  "usage": {
    "daily": 12,
    "daily_limit": 5000,
    "monthly": 340,
    "monthly_limit": 50000
  }
}

If a check fails, issues contains one or more {field, error} objects and both valid and can_send are false. A suppressed recipient that's still eligible for a retry override (see above) does not raise an issue here, since that send would actually go through; a recipient whose override is already spent, or that hasn't been retried yet, does.

validate never consumes a suppressed address's retry override or counts as an attempt against it. It only reports what would currently happen.

GET /v1/emails/:id

Retrieve a single email by its UUID, including its bodies, headers, and event history.

curl https://mail.axene.io/v1/emails/3f8c1e2a-9b4d-4c7e-8a1f-2d6b5e9c0a3d \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY"

GET /v1/emails/:id/events

List every tracked event for an email (queued, sent, delivered, opened, clicked, bounced, cancelled, and so on). Each event has an event_type, optional metadata, and a created_at timestamp.

curl https://mail.axene.io/v1/emails/3f8c1e2a-9b4d-4c7e-8a1f-2d6b5e9c0a3d/events \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY"

POST /v1/emails/:id/retry

Retry a previously failed email. Only emails with status bounced, rejected, or failed can be retried; any other status returns 409 Conflict. The retry creates a new email row linked back to the original; the original is preserved. The response shape matches POST /v1/emails/.

curl -X POST https://mail.axene.io/v1/emails/3f8c1e2a-9b4d-4c7e-8a1f-2d6b5e9c0a3d/retry \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY"

GET /v1/emails/search

Search emails by recipient, sender, subject, status, domain, or tag. The free-text q parameter accepts inline tokens that translate to structured filters:

TokenEffect
to:gmail.comSubstring match on the recipient list.
from:[email protected]Substring match on the sender address.
status:bouncedExact status match.
domain:axene.ioSubstring match on the sending domain.
tag:welcomeExact tag match.

Any remaining words become a free-text search across sender, subject, and recipients. Status and tag can also be passed as the separate query parameters status and tag. Results are paginated with page (default 0) and limit (default 20, max 100).

curl "https://mail.axene.io/v1/emails/search?q=status:bounced+to:gmail.com" \
  -H "Authorization: Bearer axm_k_YOUR_API_KEY"

GET /v1/emails/

List emails with an optional status filter and page / limit pagination.

Error handling

StatusMeaning
400 Bad RequestInvalid attachment encoding, bad send_at value, or batch size over the plan limit.
401 UnauthorizedMissing or invalid API key.
403 ForbiddenPlan feature gate or send limit reached.
404 Not FoundThe referenced email does not exist.
409 ConflictThe email cannot be retried in its current status.
413 Payload Too LargeAttachment exceeds the per-file or combined size cap.
422 Unprocessable EntityValidation error in the request body.