company logo

Help center

Go to Mailercloud
About usPricingContact us
All collectionsAPI PlatformWebhooks in API Platform

Webhooks in API Platform

Receive real-time Sent, Delivered, Open, Bounce, Spam, Unsubscribe and Reject events for your Email API and SMTP transactional emails — including your own custom data on every event.

A webhook is a URL on your server that Mailercloud calls with a JSON payload whenever something happens to one of your transactional emails — sent, delivered, opened, bounced, marked as spam, unsubscribed or rejected. It works for emails sent through both the Email API and the SMTP relay, and you can attach your own reference data to each email and get it back on every event.

Set up a webhook

In the dashboard, go to Transactional → Settings → Webhooks. You will see every webhook on your account with its URL, status and last-modified date.

Transactional Settings — Webhooks tab listing existing webhooks

Create a webhook

Click Create Webhook and fill in the form:

  • Name — a label to recognise this webhook.

  • Callback URL — the HTTPS endpoint on your server that will receive the POST requests.

  • Select event — tick the events you want delivered, or Select All Event.

Create new webhook form — Name, Callback URL and event checkboxes

Events

Each ticked event becomes a separate POST to your Callback URL. Pick only what you need.

Event

Fires when…

Sent

The message is accepted and sent from Mailercloud.

Delivery

The receiving mail server accepts the message for the recipient.

Open

The recipient opens the email. Requires a tracking domain on your sending domain.

Bounce

The message is returned — a hard bounce is a permanent failure.

Spam

The recipient or their provider marks the message as spam.

Unsubscribe

The recipient opts out.

Reject

The message is rejected before delivery, e.g. a suppressed address or a policy block.

Webhook Event Overview panel describing each event

Payload signing

Leave Payload signing on so your endpoint can verify each request genuinely came from Mailercloud. When enabled, every request carries two headers and you receive a signing secret to copy right after creating the webhook.

Events, Payload signing toggle and Add / Send sample data buttons
  • Webhook-Signature — HMAC-SHA256 of the request, keyed by your signing secret.

  • Webhook-Timestamp — Unix time the request was signed; reject requests that are too old to prevent replay.

If signing is off, the payload is byte-identical to before and no signature headers are added, so existing receivers keep working unchanged. Use Send sample data to fire a test payload at your URL before going live.

The webhook payload

Each event is delivered as an HTTP POST with a JSON body. A typical Delivered event looks like this:

{
    "event": "Delivered",
    "message_id": "CASE123-8f3a2c",
    "email": "[email protected]",
    "metadata": "example.com",
    "date_event": "2026-10-01 11:08:47",
    "ts": 1759310656,
    "ts_event": 1759310648
}
  • event — the event type: Sent, Delivered, Opened, Clicked, Bounced, Spam, Unsubscribed or Reject.

  • message_id — your messageId from the send (Email API). For SMTP sends with no Message-ID, Mailercloud generates one.

  • email — the recipient address for this event.

  • metadata — the recipient's email domain.

  • ts / ts_event — Unix timestamps: when the webhook fired and when the underlying event occurred.

  • date_event — human-readable event time in your account timezone.

Fields on specific events

  • Opened and Clicked also carry browser, device, os, userAgent and a geo object (city, country, country_code).

  • Clicked additionally carries clickedURL — the link that was clicked.

  • Bounced, Spam and Reject carry a description with the reason. A Reject caused by the suppression list also carries reason, scope and stream.

{
    "event": "Opened",
    "message_id": "CASE123-8f3a2c",
    "email": "[email protected]",
    "browser": "Chrome",
    "device": "desktop",
    "os": "Windows",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
    "geo": {
        "city": "Pune",
        "country": "India",
        "country_code": "IN"
    },
    "ts": 1759310702,
    "ts_event": 1759310701
}

Custom data on every event

Attach your own key–values to a transactional email and Mailercloud returns them on every webhook event for that message as a custom object — ideal for correlating an event back to a case id, order or user reference with no lookup on your side.

Sending custom data

Email API — add a custom object inside metadata:

"metadata": {
    "campaignType": "TRANSACTIONAL",
    "messageId": "CASE123-8f3a2c",
    "custom": {
        "user_ref": "U-45821",
        "case_id":  "C-9981",
        "branch":   "PUNE"
    }
}

SMTP relay — add one header per key, each prefixed X-MC-Custom-:

X-MC-Custom-user_ref: U-45821
X-MC-Custom-case_id:  C-9981
X-MC-Custom-branch:   PUNE

The X-MC-Custom-* headers are consumed by Mailercloud and stripped before delivery, so the recipient never sees them. SMTP header names are lowercased in transit, so a key sent over SMTP comes back lowercase; the Email API preserves your original casing.

What you get back

Every event for that message carries a custom object with your values, always returned as strings:

{
    "event": "Delivered",
    "message_id": "CASE123-8f3a2c",
    "email": "[email protected]",
    "custom": {
        "user_ref": "U-45821",
        "case_id":  "C-9981",
        "branch":   "PUNE"
    }
}

Limits

If a key breaks a rule it is dropped and named in custom_dropped — the email is always sent. Nothing about custom data can cause a send to fail.

Rule

Limit

Over-limit behaviour

Keys per message

10

First 10 (sorted by key) kept; the rest listed in custom_dropped.

Key name

≤ 50 chars · A–Z a–z 0–9 - _

Invalid key dropped and named.

Value length

≤ 256 characters

Oversized value dropped and named.

Value type

text / number / true-false

Returned as a string (123 → "123").

When keys are refused, you also get a custom_dropped array so you can spot and fix the problem:

{
    "event": "Delivered",
    "message_id": "CASE123-8f3a2c",
    "email": "[email protected]",
    "custom": { "user_ref": "U-45821" },
    "custom_dropped": ["bad key!", "case_note_2kb"]
}

If you send no custom data, there is no custom key in the payload at all, and custom_dropped appears only when something was actually refused. The reserved keys inbox_tracking, campaign_id, store_content and tags are never echoed.

Verifying the signature

When payload signing is on, verify each request before trusting it. Compute an HMAC-SHA256 over the signed payload using your secret, compare it in constant time to the Webhook-Signature header, and reject requests whose Webhook-Timestamp is too old.

const crypto = require("crypto");

function verify(req, secret) {
  const sig = req.headers["webhook-signature"];
  const ts  = req.headers["webhook-timestamp"];
  // reject anything older than 5 minutes (replay protection)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(ts + "." + req.rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Keep your signing secret private and respond 2xx quickly — do heavy work asynchronously. A non-2xx response is treated as a failed delivery and retried.

Did this answer your question?
😞
😐
😁