Sending
Send a single email or a batch of up to 50 personalised emails.
Sending is asynchronous. The API validates the request, reserves quota, queues the message and answers 202 Accepted with an email id. Delivery happens in the background. Follow it with GET /emails/{id} or a webhook.
Both endpoints on this page take a key: an email API key, or a Helipod API key with the Email API enabled (see Authentication). A dashboard session is not accepted.
Send an email
/sendcharges your walletIdempotency-KeyBody
Prop
Type
At least one of html, text or templateId is required (400 EMPTY_BODY). Send both html and text when you can: it improves deliverability.
Example
curl -X POST https://mail.helipod.io/api/email/send \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"replyTo": "[email protected]",
"subject": "Your receipt for order {{orderId}}",
"html": "<p>Hi {{name}}, thanks for order <b>{{orderId}}</b>.</p>",
"text": "Hi {{name}}, thanks for order {{orderId}}.",
"variables": { "name": "Ana", "orderId": "A-1042" },
"metadata": { "orderId": "A-1042" },
"idempotencyKey": "receipt-A-1042"
}'Response 202
{
"success": true,
"data": {
"id": "b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d",
"jobId": "eml_b7e2c1d45a6f4e3b8c9d"
}
}Prop
Type
Batch send
/send/batchcharges your walletIdempotency-KeyEach entry of messages is a complete send body (every field from Send an email, including its own idempotencyKey). Messages are processed independently: one failing does not stop the others.
Limits for one batch:
- 1 to 50 messages (
400 NO_MESSAGES,400 TOO_MANY_MESSAGES). - At most 50 recipients in total, counting
to,ccandbccof every message (400 TOO_MANY_RECIPIENTS).
curl -X POST https://mail.helipod.io/api/email/send/batch \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"templateId": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"templateData": { "name": "Ana" },
"idempotencyKey": "welcome-ana"
},
{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"templateId": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"templateData": { "name": "Budi" },
"idempotencyKey": "welcome-budi"
}
]
}'Response 202 (at least one message accepted)
results is in the same order as messages. An accepted entry has id and jobId; a rejected entry has error and code.
{
"success": true,
"data": {
"accepted": 1,
"rejected": 1,
"results": [
{ "id": "b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d", "jobId": "eml_b7e2c1d45a6f4e3b8c9d" },
{ "error": "Recipient(s) on suppression list: [email protected]", "code": "RECIPIENT_SUPPRESSED" }
]
}
}Response 400 (every message failed)
When no message was accepted, the response is a failure with code BATCH_FAILED, and data holds the per-message results as a plain array:
{
"success": false,
"error": "All messages failed",
"code": "BATCH_FAILED",
"data": [
{ "error": "Domain \"example.org\" is not added. Add and verify it before sending.", "code": "DOMAIN_NOT_FOUND" }
]
}Request-level problems (an invalid field type, more than 50 messages or recipients) reject the whole batch before any message is processed. A field that fails schema validation in any message returns 422 VALIDATION for the whole request.
Templates and variables
There are two ways to personalise content, and both use {{token}} syntax (letters, digits, _ and .; spaces inside the braces are allowed).
Without a template, put tokens in subject, html and text and supply variables. The server replaces every token before sending. If any token has no value or an empty value, the send is refused with 400 MISSING_VARIABLES, so a recipient never sees Hi ,. If the content contains tokens and you send no variables at all, the send is refused with 400 VARIABLES_REQUIRED. Literal {{tokens}} are never delivered.
With a template, pass templateId and templateData. The template's own subject is always used (a subject you pass is ignored). Every token in the template's subject and bodies must have a non-empty value in templateData (or variables), otherwise the send is refused with 400 MISSING_VARIABLES. A template send cannot carry custom headers (400 INVALID_COMBINATION). The template must belong to the key's project (404 TEMPLATE_NOT_FOUND).
See Templates to create and preview templates.
Idempotency
Network failures happen. To retry a send safely, give it an idempotencyKey. If a send with the same key was already accepted for the same API key, the API returns that earlier send (id, jobId and deduplicated: true) and does not send or charge again.
- The key is scoped to the API key that made the request. Two different API keys can use the same idempotency key independently.
- It is a body field (
idempotencyKey), not a request header. - A request that was rejected (for example
400or403) does not consume the key; a retry is processed normally. - In a batch, each message carries its own
idempotencyKey.
Recipients
Before anything is queued, the API checks every address in to, cc and bcc:
- Syntax. Malformed addresses, reserved top-level domains and documentation domains are refused with
400 INVALID_RECIPIENT. The error message lists each address and the reason. - Duplicates. Addresses are trimmed and compared case-insensitively. A person listed more than once gets one copy (in
tobeforeccbeforebcc) and is counted once. - Recipient domain. A domain that does not exist or cannot receive mail (no MX and no A/AAAA record, or a null MX) is refused with
400 RECIPIENT_DOMAIN_UNDELIVERABLE. If DNS is slow or unavailable, the check lets the send through. - Suppression. An address on this project's suppression list for a bounce or complaint, or one that hard-bounced or complained anywhere on Helipod Email, is refused with
403 RECIPIENT_SUPPRESSED. Unsubscribes do not block transactional sends; they only block broadcasts. See Suppression list. - Count. At most 50 recipients per message after duplicates are removed (
400 TOO_MANY_RECIPIENTS). Use a batch, or a broadcast, for more.
Role addresses (such as info@ or support@) and disposable inboxes are accepted for transactional sends.
Sends refused by these checks are recorded in the email log with status rejected and the reason in errorMessage, so you can find them later. They do not count against your quota.
Sender and headers
- The domain of
frommust be a domain of this project with statusverified(403 DOMAIN_NOT_FOUND,403 DOMAIN_NOT_VERIFIED). If receiving is enabled on a verified domain with a different receiving host (for examplemail.example.com), sending from that host is also allowed. subject,fromandreplyTomust not contain line breaks (400 INVALID_HEADER).- Custom
headersnames must match[A-Za-z0-9-]+and values must not contain line breaks (400 INVALID_HEADER).
Unsubscribe header
Every message sent through the API carries a List-Unsubscribe header with one-click support (RFC 8058), built for the first to address. Mail clients show their own unsubscribe button for it. No visible footer is added to API mail.
When a recipient uses it, the address is added to your project's suppression list with reason unsubscribed. That blocks broadcasts to the address but not transactional sends such as receipts or password resets.
If you set your own List-Unsubscribe in headers, yours is used instead.
Quota and billing
Quota is counted per recipient (to + cc + bcc) and reserved when the message is accepted, so concurrent requests cannot overshoot it.
- Daily limit (plans that have one, such as Free): exceeding it returns
402 DAILY_QUOTA_EXCEEDED. The day is UTC. - Monthly limit: the window starts on your subscription date, not the 1st of the month. Past the limit, sending continues only if pay-as-you-go is enabled for the project. Pay-as-you-go reserves prepaid blocks of 100 emails from your wallet deposit when needed; unused emails in a block are refunded if the plan is not renewed. With pay-as-you-go off, the send returns
402 PAYG_DISABLEDinstead of charging. A plan with no per-email price returns402 MONTHLY_QUOTA_EXCEEDED. Not enough deposit returns402 INSUFFICIENT_BALANCE.
Your current usage is in GET /status.
What is not supported
- Attachments. Rejected with
400 ATTACHMENTS_NOT_SUPPORTED. Link to the file instead. - Scheduled sends.
/senddelivers immediately. To send later, schedule a broadcast. - Tags. There is no tags field. Use
metadatato attach your own identifiers.
Secrets in emails
sensitive: true removes templateData and variables values from the job record after the send. For a send without a template, the rendered body (with the values filled in) is still stored in the email log and returned by GET /emails/{id}. To keep a secret such as a one-time code out of the log entirely, send it with a template: template sends do not store a body.