Emails
Sending is one route: POST /v1/send. Everything else on this page — templates, attachments, scheduling, batches — is a field on that same call or a variation of it.
Requires the send scope.
Send an email
curl https://api.hamanimail.com/v1/send \
-H "Authorization: Bearer hme_yourkeyid_yoursecret" \
-H "Content-Type: application/json" \
-d '{
"from": "Your Business <hello@yourbusiness.com.au>",
"to": "customer@example.com",
"subject": "Welcome",
"category": "transactional",
"html": "<p>Thanks for signing up.</p>"
}'Response 202 Accepted:
{ "status": "queued", "messageId": "…", "idempotencyKey": "…" }202 means accepted for delivery, not delivered. Delivery is reported afterwards through webhooks and the email log.
Parameters
| Field | Required | Notes |
|---|---|---|
from | yes | you@yourdomain or Name <you@yourdomain>. The domain must be one you have verified. |
to | yes | recipient address, or Name <address>. |
subject | yes | plain text. |
html | one of html / text / templateId | the HTML body. |
text | — | the plain-text body. Send it alongside html: some clients prefer it, and its absence is a spam signal. |
replyTo | — | where replies should go, if not from. |
locale | — | BCP-47 tag (en-AU, fr). Picks the language of a multi-locale template. Defaults to en. |
category | yes | a lowercase slug. transactional or marketing for most mail — see below. |
templateId + data | — | render a saved template with {{variables}} from data. |
attachments | — | see Attachments. |
idempotencyKey | — | see Safe retries. |
sendAt | — | ISO-8601 time in the future. See Scheduled sends. |
headers | — | extra headers, from a fixed allowlist. See Custom headers. |
senderIdentity | — | Australian Spam Act sender identification. See below. |
Unknown fields are rejected, not ignored. A typo like subjectt returns 400 invalid_request naming the field, rather than silently sending an email with no subject. There is no cc or bcc on this route — send one message per recipient, or use a batch.
category is not optional, and it matters
| Value | Use it for | What we do |
|---|---|---|
transactional | receipts, sign-in codes, password resets, order updates — mail the recipient asked for by doing something | delivered on the transactional path |
marketing | newsletters, announcements, promotions — mail you decided to send | a one-click unsubscribe header is added automatically, and the recipient's unsubscribe choice is honoured on every future marketing send |
Labelling marketing mail as transactional to skip the unsubscribe is a breach of the Acceptable Use Policy and of Australian law. It also damages the delivery of your genuine transactional mail, because complaints against your domain do not distinguish between the two.
Custom headers
headers takes a map of header name to value, but only from this allowlist — anything else is rejected with 400 invalid_request.
| Header | Use |
|---|---|
List-Unsubscribe, List-Unsubscribe-Post | unsubscribe. Added automatically on marketing sends; set them yourself only if you are managing your own. |
List-Id, List-Help, List-Subscribe, List-Archive, List-Owner, List-Post | mailing-list identification |
Precedence | mark bulk mail as bulk |
Auto-Submitted | mark machine-generated mail, so auto-responders stay quiet |
In-Reply-To, References | threading — reference another message so replies group correctly |
any header beginning X- | your own custom headers |
Identity and routing headers are not settable. You cannot override From, To, Subject, Reply-To or anything similar through this field — those come from the validated request fields, which is what stops a caller forging a sender.
Header values may not contain control characters or line separators. That is what blocks header injection: a value with a newline in it is rejected rather than becoming two headers.
Australian sender identification
The Spam Act 2003 requires commercial email to identify the sender and give a way to contact them. Pass senderIdentity and a compliant footer is appended to a marketing send for you.
"senderIdentity": {
"businessName": "Your Business Pty Ltd",
"abn": "48 696 864 981",
"contact": "hello@yourbusiness.com.au"
}| Field | Required | Notes |
|---|---|---|
businessName | yes | the name the recipient will recognise. |
abn | — | validated against the ATO checksum. A wrong ABN is rejected rather than printed. |
contact | — | an address or phone number the recipient can reach you on. |
This does not make your mail compliant on its own — consent and unsubscribe are still yours to get right. It handles the identification part correctly.
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | malformed JSON or a field failed validation — issues[] names the fields. |
| 400 | recipient_refused | the recipient looks undeliverable before we try — findings[] explains why. |
| 403 | from_domain_not_allowed | this key may not send from that domain. Verify the domain, or check the key's scope. |
| 409 | send_in_progress | an identical send is already in flight. Retry shortly; it will not be sent twice. |
| 413 | attachments_too_large | an attachment, or the total body, exceeds the limits. |
| 429 | rate_limited | over your per-minute limit. Back off and retry. |
| 503 | temporary | a temporary fault. Retry with the same idempotencyKey. |
The full list across every route is on the Errors page.
Attachments
"attachments": [
{ "filename": "invoice.pdf", "contentType": "application/pdf", "content": "<base64>" }
]content is the file's base64 bytes. A contentId marks a file as inline, which you then embed in the HTML as cid:<contentId>:
"attachments": [
{ "filename": "logo.png", "contentType": "image/png", "content": "<base64>", "contentId": "logo" }
]<img src="cid:logo" alt="Your Business">Limits. The request body may be up to 6 MB including the base64-encoded attachments (about 4 MB of files per message), and up to 10 MB per file is accepted by the schema, but the body cap governs. 20 files per message.
The gap between those two numbers catches people out: base64 adds about a third to a file's size, so a single 10 MB file can never fit inside a 6 MB body however the schema describes it.
Base64 grows your payload. If you are close to the cap, host the file and send a link instead — it also delivers better, because large attachments are a spam signal.
Safe retries
Pass an idempotencyKey — your order id, invoice number, or any stable string for that one message — and retrying is always safe. The same key never sends twice.
{ "from": "…", "to": "…", "subject": "Your receipt", "idempotencyKey": "order-10482" }A retried duplicate comes back as:
{ "status": "duplicate", "messageId": "…" }with the original messageId, so your records stay correct.
If you omit the key, one is derived from the message content, so accidental double-submits are still caught. Supply your own where you can — a derived key cannot tell "the customer ordered the same thing twice" from "our retry logic fired twice".
This is what makes a 503 safe: retry with the same idempotencyKey and you cannot double-send.
Send to many recipients
POST /v1/send/batch — one call, shared content, up to 500 recipients. Each recipient can carry its own data for personalisation.
{
"from": "Your Business <hello@yourbusiness.com.au>",
"subject": "Hi {{name}}",
"templateId": "welcome",
"category": "marketing",
"recipients": [
{ "to": "a@example.com", "data": { "name": "Alex" } },
{ "to": "b@example.com", "data": { "name": "Bailey" } }
]
}Each recipient is processed independently, so one bad address never stops the rest. The response carries a per-recipient result — check it, rather than assuming a 202 means all 500 were accepted.
Marketing batches must have unsubscribe configured before they will send.
Retrieve one message
GET /v1/messages/:messageIdReturns the message's current status and every delivery event recorded against it, oldest first:
{
"status": "delivered",
"events": [
{ "type": "queued", "at": 1756800000000 },
{ "type": "sent", "at": 1756800002000 },
{ "type": "delivered", "at": 1756800004000 }
]
}| Status | error | Meaning |
|---|---|---|
| 404 | message_not_found | no such message for this key — a key only ever sees its own mail. |
| 503 | messages_unavailable | could not read the log right now. Retry shortly. |