API reference
The Hamanimail sending API lets your app send email from your own domain over a simple HTTPS JSON interface. Every request and response is application/json.
You only need this if you want your software to send email (receipts, sign-up codes, notifications, newsletters). To just read and write mail as a person, use your inbox — no API key required.
Base URL
https://api.hamanimail.comAuthentication
Every request carries your API key as a bearer token:
Authorization: Bearer hme_yourkeyid_yoursecretCreate and manage keys in your workspace. A key is shown once — store it safely. Keys are scoped (send, domains, templates), so a key can be limited to exactly what it needs.
| Status | error | Meaning |
|---|---|---|
| 401 | missing_api_key | no bearer token on the request |
| 401 | invalid_api_key | key not recognised, or wrong secret |
| 401 | api_key_revoked | the key has been revoked |
| 403 | insufficient_scope | the key lacks the scope this route needs |
Send an email
POST /v1/send
curl https://api.hamanimail.com/v1/send \
-H "Authorization: Bearer hme_yourkeyid_yoursecret" \
-H "Content-Type: application/json" \
-d '{
"from": "Your Business <[email protected]>",
"to": "[email protected]",
"subject": "Welcome",
"html": "<p>Thanks for signing up.</p>"
}'Response 202:
{ "status": "queued", "messageId": "…", "idempotencyKey": "…" }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 (recommended alongside html). |
replyTo | — | where replies should go. |
category | — | transactional (default) or marketing. Marketing sends include a one-click unsubscribe automatically. |
templateId + data | — | render one of your saved templates with {{variables}} from data. |
attachments | — | see Attachments. |
idempotencyKey | — | see Safe retries. |
sendAt | — | ISO-8601 time in the future to schedule the send. |
Errors
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | malformed JSON or a field failed validation (issues[] explains). |
| 400 | recipient_refused | the recipient looks undeliverable; findings[] explains why. |
| 403 | from_domain_not_allowed | the key may not send from that domain. |
| 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 exceeds the limits below. |
| 429 | rate_limited | you sent faster than your per-minute limit. |
| 503 | (temporary) | a temporary fault — retry with the same idempotencyKey. |
Attachments
Add attachments as an array of files:
"attachments": [
{ "filename": "invoice.pdf", "contentType": "application/pdf", "content": "<base64>" }
]content is the file's base64 bytes. A contentId marks a file inline (embed it in the HTML as cid:<contentId>). Limits: the request body may be up to 6 MB including the base64-encoded attachments (about 4 MB of files per message); up to 10 MB per file is accepted by the schema, but the body cap governs; 20 files per message.
Safe retries
Pass an idempotencyKey (for example your order id) and it is always safe to retry a send: the same key never sends twice. If you omit it, one is derived from the message so accidental duplicates are still caught. A retried duplicate comes back as { "status": "duplicate" } with the original messageId.
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 <[email protected]>",
"subject": "Hi {{name}}",
"templateId": "welcome",
"category": "marketing",
"recipients": [
{ "to": "[email protected]", "data": { "name": "Alex" } },
{ "to": "[email protected]", "data": { "name": "Bailey" } }
]
}Each recipient is sent independently, so one bad address never stops the rest. The response lists a per-recipient result. Marketing batches must have unsubscribe configured.
Schedule a send
Add sendAt (ISO-8601, in the future) to POST /v1/send to park it until then:
{ "status": "scheduled", "scheduledSendId": "…", "sendAt": "2026-07-20T09:00:00.000Z" }GET /v1/scheduled— list your pending scheduled sends.GET /v1/scheduled/:id— one scheduled send.DELETE /v1/scheduled/:id— cancel one that has not yet gone out.
Templates
Save reusable, branded templates and render them at send time with templateId. Requires the templates scope.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/templates | create a template |
GET | /v1/templates | list your templates |
GET | /v1/templates/:id | fetch one |
PUT | /v1/templates/:id | replace one |
DELETE | /v1/templates/:id | delete one |
A template's subject, html and text are maps of locale → string with {{variable}} placeholders filled from the data you send.
Your sending domain
Before you can send from [email protected], verify the domain. Requires the domains scope (or set it up in your workspace, no key needed).
| Method | Path | Purpose |
|---|---|---|
POST | /v1/domains | register a domain; returns the exact DNS records to publish (DKIM, SPF, DMARC and the receiving MX). |
GET | /v1/domains/:domain | the domain's verification status. |
POST | /v1/domains/:domain/recheck | check your DNS now and confirm verification. |
Publish the returned records at your DNS provider — set the DKIM records to DNS-only — then recheck. Once verified, you can send from that domain.
Your email log
Every email you send through the API is listed for 30 days with what happened to it.
GET /v1/emails?limit=50&status=delivered&cursor=...
Authorization: Bearer hme_...Response:
{
"emails": [
{ "messageId": "…", "at": 1756800000000, "from": "[email protected]", "to": "[email protected]",
"subject": "Your receipt", "category": "transactional", "status": "delivered", "lastEventAt": 1756800004000, "detail": null }
],
"nextCursor": null,
"retentionDays": 30
}status is one of queued, sent, delivered, bounced, complained, suppressed, failed. Pass nextCursor back as cursor for older rows. The same list is in your workspace under Emails.
Webhooks
Register an https endpoint in your workspace under Webhooks and we POST a signed JSON event for each of these as they happen: email.queued, email.sent, email.delivered, email.bounced, email.complained, email.suppressed, email.failed.
{ "type": "email.delivered", "at": 1756800004000,
"data": { "messageId": "…", "from": "[email protected]", "to": "[email protected]",
"subject": "Your receipt", "category": "transactional", "status": "delivered", "detail": null } }Each request carries X-Hamani-Event and X-Hamani-Signature: t=<unix-ms>,v1=<hex>. Verify it with the signing secret shown once when you added the endpoint: compute HMAC-SHA256 over t + "." + rawBody, compare to v1 in constant time, and reject a t more than five minutes old.
const [t, v1] = sig.split(",").map((p) => p.split("=")[1]);
const mac = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1)) && Math.abs(Date.now() - Number(t)) < 300000;Delivery is best-effort: if your endpoint is down or takes more than 5 seconds to answer, we log it and move on, and your email log above is always the record. Answer with any 2xx quickly and do the work afterwards.
Rate limits
- 120 requests per minute per API key. Over the limit returns
429 rate_limited— slow down and retry. - Request body up to 6 MB, including any base64-encoded attachments (see Attachments).
Sending responsibly
Only send to people who expect to hear from you. Bounces and spam complaints are suppressed automatically, so a bad address or a complaint stops future sends to that recipient. Marketing email carries a one-click unsubscribe, as Australian law requires. Repeatedly sending unwanted mail can pause your sending.