API reference

How to send email through Hamanimail from your own domain.

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.com

Authentication

Every request carries your API key as a bearer token:

Authorization: Bearer hme_yourkeyid_yoursecret

Create 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.

StatuserrorMeaning
401missing_api_keyno bearer token on the request
401invalid_api_keykey not recognised, or wrong secret
401api_key_revokedthe key has been revoked
403insufficient_scopethe 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

FieldRequiredNotes
fromyesyou@yourdomain or Name <you@yourdomain>. The domain must be one you have verified.
toyesrecipient address, or Name <address>.
subjectyesplain text.
htmlone of html / text / templateIdthe HTML body.
textthe plain-text body (recommended alongside html).
replyTowhere replies should go.
categorytransactional (default) or marketing. Marketing sends include a one-click unsubscribe automatically.
templateId + datarender one of your saved templates with {{variables}} from data.
attachmentssee Attachments.
idempotencyKeysee Safe retries.
sendAtISO-8601 time in the future to schedule the send.

Errors

StatuserrorMeaning
400invalid_requestmalformed JSON or a field failed validation (issues[] explains).
400recipient_refusedthe recipient looks undeliverable; findings[] explains why.
403from_domain_not_allowedthe key may not send from that domain.
409send_in_progressan identical send is already in flight — retry shortly; it will not be sent twice.
413attachments_too_largean attachment or the total exceeds the limits below.
429rate_limitedyou 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" }

Templates

Save reusable, branded templates and render them at send time with templateId. Requires the templates scope.

MethodPathPurpose
POST/v1/templatescreate a template
GET/v1/templateslist your templates
GET/v1/templates/:idfetch one
PUT/v1/templates/:idreplace one
DELETE/v1/templates/:iddelete 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).

MethodPathPurpose
POST/v1/domainsregister a domain; returns the exact DNS records to publish (DKIM, SPF, DMARC and the receiving MX).
GET/v1/domains/:domainthe domain's verification status.
POST/v1/domains/:domain/recheckcheck 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

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.