Webhooks
Receive delivery, engagement, failure and inbound-mail events on your own HTTPS endpoint.
Register an HTTPS endpoint and Helipod Email will POST a signed JSON event to it when something happens to your project's mail: delivered, bounced, opened, received and more.
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.
Events
| Event | When |
|---|---|
email.sent | A message was handed over for delivery. |
email.delivered | The recipient's mail server accepted it. |
email.bounced | The recipient's server refused it. |
email.complained | The recipient marked it as spam. |
email.opened | The recipient opened it. |
email.clicked | The recipient clicked a link. |
email.rejected | It was rejected at delivery time (for example a content problem). |
email.failed | Sending gave up after retries. A broadcast sends one aggregated email.failed per run, listing up to 100 failed recipients. |
email.received | A message arrived at one of your receiving addresses. See Inbox. |
Payload
Every delivery is a POST with Content-Type: application/json:
{
"type": "email.delivered",
"createdAt": "2026-10-16T09:14:10.004Z",
"data": {
"messageId": "01a9f3c2b7d4e5f6a8b9c0d1e2f3a4b5",
"recipients": ["[email protected]"],
"notification": { }
}
}Prop
Type
What data.notification contains depends on the event:
email.sent,email.delivered,email.bounced,email.complained,email.opened,email.clicked,email.rejected: a summary of the delivery event. Fields appear only for the matching event:
{
"type": "bounce",
"mail": { "messageId": "0100019a…", "timestamp": "2026-10-16T10:02:11.000Z", "subject": "Your receipt", "destination": ["[email protected]"] },
"bounce": { "bounceType": "Permanent", "bounceSubType": "General", "timestamp": "…", "recipients": [{ "emailAddress": "[email protected]", "status": "5.1.1", "action": "failed", "diagnosticCode": "smtp; 550 5.1.1 user unknown" }] }
}Other events carry delivery (timestamp, processingTimeMillis, recipients), complaint (complaintFeedbackType, recipients), open and click (timestamp, userAgent, link for clicks), reject (reason) or deliveryDelay.
email.failedfor a single send:{ "error": "...", "jobId": "..." }.email.failedfor a broadcast:{ "broadcastId": "...", "failed": 3, "sent": 1237, "error": "send_error" }.email.received: a summary of the received message:
{
"id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
"recipient": "[email protected]",
"deliveredTo": ["[email protected]"],
"mailbox": "support",
"from": "[email protected]",
"fromName": "Ana Putri",
"to": ["[email protected]"],
"cc": [],
"subject": "Question about my order",
"snippet": "Hi, I ordered on Monday and ...",
"textBody": "Hi, I ordered on Monday and ...",
"hasAttachments": true,
"attachments": [{ "index": 0, "filename": "receipt.pdf", "contentType": "application/pdf", "size": 48213 }],
"folder": "inbox",
"spamVerdict": "PASS",
"virusVerdict": "PASS",
"receivedAt": "2026-10-16T10:02:11.000Z"
}textBody is cut at 64 KB. Use id with GET /inbound/messages/{id} for the full message and attachments.
Verify the signature
Every request carries a Helipod-Signature header:
Helipod-Signature: t=1760605450,v1=5f2b0c7e9a1d...tis the Unix time (seconds) the event was signed.v1is the hex HMAC-SHA256 of the string<t>.<raw request body>, keyed with your webhooksecret.
Compute the HMAC over the raw body bytes, before any JSON parsing, compare it in constant time, and reject old timestamps to block replays.
import crypto from "node:crypto";
export function verifyHelipodSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Delivery and retries
- Respond with any
2xxwithin 8 seconds. - A network error, a timeout or a
5xxresponse is retried up to 3 attempts in total, 1 and then 2 seconds apart. Retries carry the same body and signature. - A
4xxresponse is not retried. - Events can arrive more than once and out of order. Make your handler idempotent (for example key on
type+messageId+ recipient). - The endpoint's host is resolved before each delivery and must be a public address; deliveries to private or internal addresses are refused.
failCounton the webhook counts consecutive failed deliveries and resets on the next success.
List webhooks
/webhookscurl https://mail.helipod.io/api/email/webhooks \
-H "Authorization: Bearer your_email_api_key"{
"success": true,
"data": [
{
"id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
"url": "https://hooks.example.com/helipod-email",
"events": ["email.delivered", "email.bounced", "email.complained"],
"enabled": true,
"failCount": 0,
"lastTriggeredAt": "2026-10-16T09:14:10.004Z",
"createdAt": "2026-10-01T08:00:00.000Z"
}
]
}Create a webhook
/webhooksProp
Type
curl -X POST https://mail.helipod.io/api/email/webhooks \
-H "Authorization: Bearer your_email_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/helipod-email",
"events": ["email.delivered", "email.bounced", "email.complained", "email.received"]
}'Response 201:
{
"success": true,
"data": {
"id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
"url": "https://hooks.example.com/helipod-email",
"events": ["email.delivered", "email.bounced", "email.complained", "email.received"],
"enabled": true,
"failCount": 0,
"lastTriggeredAt": null,
"createdAt": "2026-10-16T09:30:00.000Z",
"secret": "whsec_3f9a1c2b7d4e5f6a8b9c0d1e2f3a4b5c"
}
}Store secret now: it is not returned again. Errors: 400 INVALID_URL, 400 NO_EVENTS, 402 WEBHOOK_LIMIT_REACHED.
There is no update endpoint. To change the URL or events, create a new webhook and delete the old one.
Delete a webhook
/webhooks/{id}curl -X DELETE https://mail.helipod.io/api/email/webhooks/8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e \
-H "Authorization: Bearer your_email_api_key"{ "success": true, "data": { "deleted": true } }Returns 404 WEBHOOK_NOT_FOUND for an unknown id.