Logs and analytics
Follow each email's delivery, read project analytics and usage, and manage the suppression list.
All endpoints on this page accept an email API key (or a Helipod API key with the Email API enabled) and act on the key's project.
Email log
Every message accepted by POST /send, POST /send/batch or a broadcast, and every send the API refused before queueing, is recorded in the email log.
Email statuses
status | Meaning |
|---|---|
queued | Accepted, waiting to be handed over for delivery. |
sent | Handed over for delivery. |
delivered | The recipient's mail server accepted it. |
bounced | The recipient's server refused it. The address is suppressed. |
complained | The recipient marked it as spam. The address is suppressed. |
rejected | Refused, either by the API before queueing (the reason is in errorMessage) or at delivery time. |
failed | Could not be sent after retries. The reason is in errorMessage. |
lastEvent holds the type of the most recent event, for example send, delivery, open, click, bounce.
List emails
/emails| Query | Type | Notes |
|---|---|---|
status | string | One of the statuses above. |
search | string | Subject or recipient contains this text, or an exact messageId. Up to 200 characters. |
from | string | ISO date-time. Only emails created at or after it. |
to | string | ISO date-time. Only emails created at or before it. |
limit | number | 1 to 200, default 50. |
cursor | string | nextCursor from the previous page. |
curl "https://mail.helipod.io/api/email/emails?status=bounced&from=2026-10-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"data": [
{
"id": "b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d",
"messageId": "01a9f3c2b7d4e5f6a8b9c0d1e2f3a4b5",
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Your receipt for order A-1042",
"status": "bounced",
"lastEvent": "bounce",
"hasAttachments": false,
"sentAt": "2026-10-16T09:14:04.010Z",
"deliveredAt": null,
"bouncedAt": "2026-10-16T09:15:12.000Z",
"errorMessage": null,
"createdAt": "2026-10-16T09:14:03.221Z"
}
],
"nextCursor": null
}
}messageId is the delivery message id. It is also what webhook events carry, so you can find an email from a webhook with ?search=<messageId>. It is null until the message has been handed over for delivery.
Errors: 400 INVALID_CURSOR.
Get one email
/emails/{id}curl https://mail.helipod.io/api/email/emails/b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"id": "b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d",
"messageId": "01a9f3c2b7d4e5f6a8b9c0d1e2f3a4b5",
"from": "[email protected]",
"to": ["[email protected]"],
"cc": null,
"bcc": null,
"subject": "Your receipt for order A-1042",
"replyTo": "[email protected]",
"body": "<p>Hi Ana, thanks for order <b>A-1042</b>.</p>",
"status": "delivered",
"lastEvent": "open",
"hasAttachments": false,
"attachmentMeta": null,
"metadata": { "orderId": "A-1042" },
"errorMessage": null,
"sentAt": "2026-10-16T09:14:04.010Z",
"deliveredAt": "2026-10-16T09:14:09.880Z",
"bouncedAt": null,
"complainedAt": null,
"createdAt": "2026-10-16T09:14:03.221Z",
"events": [
{ "type": "send", "recipient": "[email protected]", "timestamp": "2026-10-16T09:14:04.010Z", "bounceType": null, "bounceSubType": null, "userAgent": null, "link": null },
{ "type": "delivery", "recipient": "[email protected]", "timestamp": "2026-10-16T09:14:09.880Z", "bounceType": null, "bounceSubType": null, "userAgent": null, "link": null },
{ "type": "open", "recipient": "[email protected]", "timestamp": "2026-10-16T09:31:40.112Z", "bounceType": null, "bounceSubType": null, "userAgent": "Mozilla/5.0", "link": null }
]
}
}bodyis the HTML (or, if there was no HTML, the text) of a send without a template, with variables already filled in. Template sends store no body:bodyisnullandmetadatacarriestemplateIdandtemplateName.metadatais what you passed on send. For template sends,templateId,templateNameandsourceare added and cannot be overridden.events[].typeis one ofsend,delivery,bounce,complaint,open,click,reject,failed. Bounces carrybounceType(PermanentorTransient) andbounceSubType; clicks carrylink.
Returns 404 EMAIL_NOT_FOUND for an unknown id.
Get email events
/emails/{id}/eventscurl https://mail.helipod.io/api/email/emails/b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d/events \
-H "Authorization: Bearer your_email_api_key"{ "success": true, "data": { "id": "b7e2c1d4-5a6f-4e3b-8c9d-0e1f2a3b4c5d", "events": [ ] } }Analytics
/analytics| Query | Type | Notes |
|---|---|---|
period | today | 7d | 30d | 90d | Default 7d. Days are UTC. An unknown value is treated as 7d. |
curl "https://mail.helipod.io/api/email/analytics?period=30d" \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": {
"period": "30d",
"start": "2026-09-17",
"totals": {
"sent": 1200, "delivered": 1180, "bounced": 12, "hardBounced": 8, "softBounced": 4,
"complained": 1, "rejected": 3, "opened": 610, "clicked": 140,
"received": 0, "receivedBytes": 0
},
"rates": {
"deliveryRate": "98.33", "bounceRate": "1.00", "hardBounceRate": "0.67",
"complaintRate": "0.08", "openRate": "51.69", "clickRate": "11.86"
},
"limits": { "bounce": 0.02, "complaint": 0.0008 },
"receiving": null,
"series": [
{ "date": "2026-10-16", "sent": 40, "delivered": 39, "bounced": 1, "hardBounced": 1, "complained": 0, "opened": 20, "clicked": 4, "received": 0, "receivedBytes": 0 }
]
}
}- Sending numbers are grouped by the day the email was sent: a bounce or open that arrives the next day still counts toward the send day.
ratesare percentages as strings with two decimals.complaintRate,openRateandclickRateare relative to delivered emails (or sent, when nothing was delivered).limitsare the bounce and complaint rates (as fractions) above which sending for the project is suspended. See Reputation.receivingis present when the plan includes an inbox:{ cycleStart, cycleReceivedBytes, cycleLimitBytes }.
Project status
/statuscurl https://mail.helipod.io/api/email/status \
-H "Authorization: Bearer your_email_api_key"Abridged response:
{
"success": true,
"data": {
"activated": true,
"configSet": {
"sendingEnabled": true,
"status": "active",
"autoDisabled": false,
"autoDisabledReason": null,
"dailySendCount": 40,
"dailySendLimit": 0,
"bounceRate": "0.0042",
"complaintRate": "0.0000"
},
"platformPaused": null,
"plan": { "code": "STARTER", "monthlySendLimit": 50000, "dailySendLimit": 0 },
"subscription": { "status": "active", "expiredAt": "2026-11-01T00:00:00.000Z", "autoRenew": true, "paygEnabled": false },
"usage": { "today": 40, "month": 1200, "dailyLimit": 0, "monthlyLimit": 50000, "dailyRemaining": 0 },
"domains": { "total": 1, "verified": 1 },
"keys": { "total": 2 },
"contacts": { "total": 3400, "subscribed": 3310 },
"broadcasts": { "allowBroadcasts": true, "active": 0 }
}
}Useful fields:
usage: emails counted today (UTC) and in the current monthly window, with the limits. A limit of0means no limit of that kind on your plan (dailyRemainingis then0too and can be ignored).configSet.autoDisabledandautoDisabledReason: the project is suspended for its bounce or complaint rate.configSet.bounceRateandcomplaintRate: the rolling rates as fractions.platformPaused: a reason string when sending is paused for every project on Helipod Email, otherwisenull.plan: the active plan's limits and feature flags (for examplemaxDomains,maxTemplates,maxContacts,allowBroadcasts), ornullwithout a plan.subscription: the latest subscription, including whether pay-as-you-go (paygEnabled) is on.
Suppression list
The suppression list holds addresses this project will not mail. Entries are added automatically on a hard bounce (permanent), a soft bounce (30 days), a spam complaint (permanent) and an unsubscribe (permanent), and you can add your own.
reason | Blocks transactional sends | Blocks broadcasts | Expires |
|---|---|---|---|
hard_bounce | yes | yes | never |
complaint | yes | yes | never |
soft_bounce | yes | yes | after 30 days |
unsubscribed | no | yes | never |
In addition, an address that really hard-bounced or complained in any Helipod Email project cannot receive transactional sends from yours either; the error says only that the address previously bounced or complained. Entries you add yourself apply to your project only.
List suppressions
/suppressions| Query | Type | Notes |
|---|---|---|
limit | number | 1 to 500, default 100. Not paginated. |
curl "https://mail.helipod.io/api/email/suppressions?limit=200" \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": [
{
"id": "7c6b5a4f-3e2d-4c1b-8a09-f8e7d6c5b4a3",
"email": "[email protected]",
"reason": "hard_bounce",
"source": "bounce",
"suppressedAt": "2026-10-10T03:12:44.000Z",
"expiresAt": null,
"permanent": true
}
]
}source tells where the entry came from, for example bounce, complaint, unsubscribe, unsubscribe_link or manual.
Add a suppression
/suppressionsProp
Type
curl -X POST https://mail.helipod.io/api/email/suppressions \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","reason":"unsubscribed"}'{ "success": true, "data": { "id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d", "email": "[email protected]", "reason": "unsubscribed" } }Response status is 201.
Remove a suppression
/suppressions/{id}curl -X DELETE https://mail.helipod.io/api/email/suppressions/2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d \
-H "Authorization: Bearer your_email_api_key"{ "success": true, "data": { "deleted": true } }Only remove an entry when you are sure the address works again and the person wants your mail. Returns 404 SUPPRESSION_NOT_FOUND for an unknown id.