HyoNovaDocumentation

Webhooks

Receive signed Hyo Mail events at your HTTPS endpoint — delivery, engagement, contact, conversion and store events — verify the signature, and handle retries.

AvailableWorks today for workspaces whose plan includes it.ReferenceFor: Developers, IT & security, AdminsLast updated: Introduced:
On this page

Webhooks let HyoNova tell your systems when something happens, instead of your systems asking repeatedly. They are part of Hyo Mail: they report events for messages sent through the Mail API, for campaign recipients, for contacts, and for orders from a connected store.

Who can set them up#

Creating, editing and deleting webhooks needs Mail administration — by default Owners and Admins.

Create a webhook#

  1. Open the webhooks. Go to Mail › Automations and open the Webhooks tab.
  2. Add an endpoint. Choose New webhook. Enter the URL of your endpoint and an optional description.
  3. Choose the events. Tick the Events you want. An endpoint receives only the events it subscribed to.
  4. Copy the secret. After saving, Webhook secret is shown once. Store it in your secret manager; you need it to verify signatures.

The endpoint must use https:// and resolve to a public internet address. Private, loopback and internal hosts are refused with WEBHOOK_HOST_NOT_ALLOWED when you save, and checked again before every delivery.

Edit webhook changes the URL, the events or Enabled. Deliveries lists recent deliveries with the event, Status, Attempts and the HTTP status your endpoint returned.

Rotate the secret#

Rotate secret issues a new secret and shows it once. For the next 24 hours, every delivery is signed with both the new and the previous secret — the header then carries two v1 values — so you can switch your endpoint over without losing events. After 24 hours, only the new secret signs.

Events#

EventSent whendata fields
sentThe email service accepted a messageTransactional: messageId, email, occurredAt. Campaign: campaignId, recipientId, contactId, email, occurredAt
deliveredThe recipient's mail server accepted itAs for sent
bouncedIt bounced permanently; the address is suppressedAs for sent
complaintThe recipient reported it as spam; the address is suppressedAs for sent
failedIt was not sent, or the email service refused itAs for sent, plus error for transactional messages
openedA campaign recipient opened the email for the first timeCampaign fields
clickedA campaign recipient clicked a tracked link for the first timeCampaign fields, plus linkId
unsubscribedA campaign recipient unsubscribedCampaign fields
message.acceptedThe email service accepted a transactional messagemessageId, email, providerMessageId, occurredAt
campaign.sentA campaign finished sendingcampaignId, accepted, failed, suppressed, skipped, unknown, occurredAt
campaign.clickEvery click on a tracked campaign link, not only the firstCampaign fields, plus linkId and suspect (true when the click looks automated)
contact.updatedA contact was created or changed through the API or a store connectioncontactId, email, status, source, change (created, updated or consent), occurredAt
conversion.recordedA conversion was recorded or adjustedconversion; a refund also has adjustment and netRevenue
order.eventA connected store sent any order eventintegrationId, provider, order
order.createdA connected store reported a new orderAs for order.event
order.paidA connected store reported a paid orderAs for order.event

The request#

HyoNova sends each event as an HTTP POST with a JSON body:

http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: HyoNova-Mail-Webhooks/1.0
HyoNova-Signature: t=1792270000,v1=5d41402abc4b2a76b9719d911017c592...
HyoNova-Delivery: 0f8e7d6c-5b4a-4321-9876-0123456789ab
HyoNova-Attempt: 1
json
{
  "id": "0f8e7d6c-5b4a-4321-9876-0123456789ab",
  "event": "delivered",
  "occurredAt": "2026-10-18T09:30:05.000Z",
  "data": {
    "messageId": "6f1c2d4e-9a7b-4c1d-8e2f-3a4b5c6d7e8f",
    "email": "name@example.com",
    "occurredAt": "2026-10-18T09:30:05.000Z"
  }
}
  • id is the delivery ID. It is also in the HyoNova-Delivery header and stays the same when a delivery is retried, so use it to ignore duplicates.
  • HyoNova-Attempt is the attempt number, starting at 1.

Verify the signature#

HyoNova-Signature has the form t=<unix seconds>,v1=<signature>. The signature is the hex HMAC-SHA256 of the string <t>.<raw request body>, keyed with your webhook secret. During a secret rotation the header has two v1 values; accept the request if either one matches.

To verify a request:

  1. Read the raw body exactly as received, before any JSON parsing.
  2. Compute HMAC-SHA256 of t + "." + body with your secret, as hex.
  3. Compare it with each v1 value in constant time. Reject the request if none matches.
  4. Reject requests whose t is more than five minutes away from your clock, to block replays.
Node.js / TypeScript
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';

export function verifyHyoMailSignature(rawBody: string, header: string, secret: string, toleranceSeconds = 300): boolean {
  const parts = header.split(',').map((p) => p.trim());
  const t = parts.find((p) => p.startsWith('t='))?.slice(2) ?? '';
  const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
  if (!/^\d+$/.test(t) || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return signatures.some((s) => {
    const given = Buffer.from(s, 'hex');
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const raw = Buffer.concat(chunks).toString('utf8');
    const header = String(req.headers['hyonova-signature'] ?? '');
    if (!verifyHyoMailSignature(raw, header, process.env.HYO_MAIL_WEBHOOK_SECRET!)) {
      res.writeHead(400).end();
      return;
    }
    const event = JSON.parse(raw); // { id, event, occurredAt, data }
    // Ignore event.id if you have already handled it, then queue the work.
    res.writeHead(200).end();
  });
}).listen(3000);

Respond and retries#

  • Answer with any 2xx status within 10 seconds. Do slow work after you respond.
  • Redirects are not followed; point the webhook at the final URL.
  • Any other answer, a timeout or a network error counts as a failed attempt. HyoNova makes up to 5 attempts in total, waiting about 1 minute, 5 minutes, 15 minutes and 1 hour between them.
  • After the fifth failed attempt, the delivery moves to the dead-letter state (dead_letter) and is not retried automatically. HyoNova keeps it for a manual retry, but the Deliveries list does not offer a retry button yet. Fix your endpoint; new events are delivered as usual.
  • Deliveries can arrive more than once and not always in order. Make your handling idempotent with the delivery id, and use occurredAt to order events.
  • HyoNova records only the HTTP status your endpoint returned, never the response body.

Status#

FeatureStatusNotes
Signed webhooksAvailableWorks today for workspaces whose plan includes it.Receive signed Hyo Mail delivery, engagement, contact and store events at your HTTPS endpoint, with automatic and manual retries.
↑↓ to navigate · Enter to open · Esc to close