Gareth Wilson Gareth Wilson

Guide to Lettermint Webhooks: Features and Best Practices

Published


Lettermint is a European transactional email service: your application sends password resets, receipts and notifications through its API or SMTP, and Lettermint handles delivery. What happens after sending is reported through webhooks: delivery, bounces, spam complaints, opens and clicks, suppression changes and parsed inbound mail. A webhook replaces polling the Team API for status updates.

Read on for how Lettermint webhooks are scoped, what each event carries, how to verify X-Lettermint-Signature, and how its retry, pause and disable rules behave when your endpoint is down.

What are Lettermint webhooks?

A Lettermint webhook has one URL, one whsec_ signing secret and a scope: the whole team, selected projects, or selected routes. Within that scope it receives the events you choose. A delivery mode setting decides whether it gets Live events, Sandbox events or both, and new webhooks default to Live.

You can run several webhooks when different systems need different events, or one webhook with several targets when one system handles events from several projects or routes. The context object in each event tells you which team, project and route it came from.

Lettermint webhook features

FeatureDetails
Signaturehex HMAC-SHA256
Signature headerx-lettermint-signature
BodyJSON
Event routingBody field event

Delivery rules from Lettermint's webhook documentation:

  • URLs must be public HTTPS addresses, and Lettermint doesn't follow redirects.
  • Requests time out after 30 seconds.
  • Timeouts, 408, 429 and 5xx are retried: 12 attempts over roughly 14 hours, from 1 minute up to 6 hours apart. Other 4xx, including 401, are not retried.
  • 20 unanswered deliveries in a row pause the webhook for a minute, doubling up to 5 minutes.
  • A webhook whose deliveries all fail for 72 hours is disabled, and events during that time are never sent.

Events and payloads

| Event routing | Body field event |

Every event has id, a UUIDv4 for that event, plus event, timestamp, context and data. Outbound message events add sandbox and sandbox_result, so a handler can skip customer-facing actions for test messages. Message events carry message_id, subject, recipient, metadata, tag and tags in data, and suppression events carry suppression_id, type, value, reason and applies_to.

This is the message.clicked sample from the webhook samples library:

{
  "id": "b2c3d4e5-f6a7-8901-bcde-f01234567892",
  "event": "message.clicked",
  "timestamp": "2025-08-08T20:18:30.000Z",
  "data": {
    "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "subject": "Your weekly recommendations",
    "metadata": {
      "X-Campaign-ID": "weekly-recs"
    },
    "tag": "recommendations",
    "tags": [
      {
        "name": "__lettermint_tag",
        "value": "recommendations"
      },
      {
        "name": "variant",
        "value": "B"
      }
    ],
    "recipient": "user@example.com",
    "clicked_at": "2025-08-08T20:18:30+00:00",
    "tracking_event_id": "0198a5be-cfa8-78e4-8339-144ab5a92fb1",
    "classification_revision": 1,
    "destination_url": "https://example.com/product/123",
    "link_index": 0,
    "anchor_text": "View Product",
    "first_click": true,
    "device_type": "mobile",
    "client_type": "browser",
    "client_name": "Safari",
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15",
    "bot": {
      "detected": false,
      "probability": 0,
      "classification": "genuine",
      "proxy_type": null,
      "reason_codes": [],
      "machine": false,
      "counts_for_metrics": true
    }
  }
}

The bot object classifies the click. With Include machine events off, security scanners, link previews and generic bots are left out of message.opened and message.clicked webhooks. metadata returns the values you attached when sending, which is the easiest way to join an event back to your own records.

Setting up Lettermint webhooks

  1. In Dashboard > Webhooks, click Create webhook.
  2. Enter a Name and the HTTPS URL, for example https://example.com/webhooks/lettermint.
  3. Optionally add Basic Auth if your endpoint requires a login. Lettermint encrypts the credentials and never returns them.
  4. Choose the Scope: the team, specific projects or specific routes.
  5. Select events such as message.delivered, message.hard_bounced, message.spam_complaint, message.failed and suppression.added. Leave Include machine events off unless you want bot observations.
  6. Save, open the webhook's details and copy the secret into your configuration as LETTERMINT_WEBHOOK_SECRET.

Paste the whole secret, including the whsec_ prefix. Lettermint uses it as a plain HMAC key.

The same can be done with POST /v1/webhooks on the Team API. Changing a webhook's targets doesn't change its secret.

To use Hookdeck, create a Lettermint source with that secret, register the source URL as the webhook URL, then click Test Webhook to send webhook.test. Lettermint won't deliver to localhost, so for local work run:

npx hookdeck-cli listen 3000 lettermint --path /webhooks/lettermint

Securing Lettermint webhooks

Parse t and every v1 from X-Lettermint-Signature, compute a hex HMAC-SHA256 over {t}.{raw_body} with the full secret, and accept the request if any v1 matches in constant time. Lettermint's signing page recommends a 300-second tolerance on t, and its SDKs also require X-Lettermint-Delivery to equal t.

Node.js verification

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-lettermint-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const parts = signature.split(',').map((part) => part.trim());
  const times = parts.filter((part) => part.startsWith('t=')).map((part) => part.slice(2));
  if (times.length !== 1 || !/^[0-9]+$/.test(times[0])) return false;
  const message = Buffer.concat([Buffer.from(`${times[0]}.`, 'utf8'), rawBody]);
  const candidates = parts.filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

This verifier omits the time check. That matters if Hookdeck sits in front: Hookdeck forwards the original headers, so a delivery it retries or you replay later carries the original t, and the SDK's default 300-second tolerance would reject it. Verify at Hookdeck, or widen the tolerance in any downstream check.

The handler records the event, flags sandbox traffic and leaves list updates to a worker:

const express = require('express');

const app = express();

app.post('/webhooks/lettermint', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.LETTERMINT_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!event || typeof event !== 'object' || Array.isArray(event)) return res.sendStatus(400);
  try {
    if (event.event === 'webhook.test') return res.sendStatus(200);

    await recordEmailEvent(event.id, {
      type: event.event,
      recipient: event.data?.recipient ?? null,
      messageId: event.data?.message_id ?? null,
      sandbox: event.sandbox === true,
      context: event.context,
      data: event.data,
    });
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

recordEmailEvent ignores an id it has already stored. Suppressing an address after message.hard_bounced or message.spam_complaint happens in the worker, and only when sandbox is false.

Hookdeck's Lettermint source checks the same {t}.{raw_body} HMAC, accepting any matching v1, with no age limit. It answers Lettermint with 200 and records signature failures as failed verification.

Make Lettermint webhooks production-ready. Hookdeck Event Gateway answers every Lettermint delivery, so your webhook is never paused or disabled, and holds bounce and complaint events until your mailing list service is ready.

Lettermint webhook limitations and pain points

A webhook can be disabled, losing events

The Problem: After a long outage, a webhook stops sending and the events from that period never arrive.

Why It Happens: Lettermint disables a webhook when every delivery has failed for 72 hours. Events that occur while it's disabled are not delivered or queued.

Workarounds:

  • Act on Lettermint's disable email and turn the Enabled toggle back on once your endpoint returns 2xx.
  • Reconcile message status through the Team API for the affected period.

How Hookdeck Can Help: Hookdeck accepts every delivery, so Lettermint never sees 72 hours of failures, and Hookdeck keeps retrying your service for as long as you configure.

Pauses when your endpoint stops answering

The Problem: Deliveries are skipped while the webhook is paused.

Why It Happens: After 20 deliveries in a row with no response, Lettermint pauses the webhook for 1 minute, then 2, up to 5. Deliveries that come up during a pause are recorded as skipped attempts.

Workarounds:

  • Make sure the endpoint always returns some HTTP response quickly, even an error, rather than timing out.
  • Keep processing out of the request so the 30-second timeout is never reached.

How Hookdeck Can Help: Hookdeck responds to Lettermint immediately, so connection failures and timeouts on your side never reach Lettermint's pause logic.

Most 4xx responses end delivery

The Problem: A misrouted request or a validation error in your framework drops the event.

Why It Happens: Lettermint retries only timeouts, 408, 429 and 5xx. Every other 4xx, 401 included, is final. Manual retry from the dashboard or API is still possible for failed deliveries.

Workarounds:

  • Return 5xx for temporary problems and reserve 4xx for requests you never want again.
  • Retry failed deliveries from the dashboard once fixed.

How Hookdeck Can Help: Hookdeck's own retry rules decide what happens when your service returns an error, and failed events can be replayed in bulk.

Strict timestamp checks in the SDKs

The Problem: Verification with Lettermint's SDK fails for events retried by a gateway or replayed later.

Why It Happens: The SDKs reject a t more than 300 seconds from now by default and require X-Lettermint-Delivery to match it.

Workarounds:

  • Raise the SDK's tolerance for downstream verification.
  • Or verify once where requests enter your system and pass trusted events on internally.

How Hookdeck Can Help: Hookdeck verifies the signature when the request arrives, without any age limit, so your service can trust what Hookdeck delivers.

Best practices

Keep the whsec_ prefix

The whole string is the key. Stripping the prefix or base64-decoding the rest produces a different key and every check fails.

Separate sandbox traffic

Use sandbox and sandbox_result to stop test messages from unsubscribing or suppressing real customers.

Deduplicate on id

Retries and manual retries resend the same event. Store id as described in the webhook idempotency guide.

Acknowledge, then act

Suppression updates and CRM syncs run better in a worker, following the case for asynchronous processing.

Conclusion

Lettermint webhooks report what happens to every email after sending, signed over a timestamp and the body in X-Lettermint-Signature. Verify with the full whsec_ secret, deduplicate on id, keep sandbox events apart, and always answer quickly, because sustained failures pause the webhook and 72 hours of them disable it.

Hookdeck Event Gateway keeps your Lettermint webhook healthy while your own services come and go.

Get started with Hookdeck for free and handle Lettermint webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.