Early accessSome features, including AI drafting, are still being switched on.See what works today

Setup guide · Data out

Receive signed webhooks

Sendalto can forward selected events — from the Events API or HubSpot sync — to your endpoint. Each request is signed, so you can prove it came from Sendalto.

Checked against Sendalto on

1. Add a subscription

  1. In Settings → API & privacy add a webhook with your endpoint URL and the exact event names to forward, such as trial_started or crm.contact.changed (up to 50).
  2. Copy the signing secret. It is shown once; store it on your server.
  3. Only events recorded after the subscription is created are delivered; history is not replayed.

The URL must be public https:// on port 443 with a host name — no IP addresses, private networks, logins in the URL or #fragment. Redirects are not followed.

2. What a delivery looks like

Request
POST /sendalto-webhook HTTP/1.1
content-type: application/json
x-sendalto-signature: t=1791279000,v1=5f0c…e9a1
x-sendalto-delivery: 0b9d7c52-…

{
  "id": "6c7e1d3a-…",
  "type": "trial_started",
  "occurredAt": "2026-10-06T09:30:00.000Z",
  "data": {
    "email": "[email protected]",
    "properties": { "plan": "team", "seats": 5 }
  }
}
  • id is the event ID, type the event name, occurredAt the event time; data carries the person’s email and the event properties, so treat deliveries as personal data.
  • x-sendalto-delivery identifies this delivery and stays the same on retries.
  • x-sendalto-signature is t=<unix seconds>,v1=<hex HMAC>.

3. Verify the signature

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Split the header into t and v1. Reject the request if t is more than about 5 minutes from your clock.
  3. Compute HMAC-SHA256 with your signing secret over the string <t>.<raw body> and hex-encode it.
  4. Compare it with v1 in constant time. Reject on mismatch.
  5. Skip deliveries whose x-sendalto-delivery you have already processed, then answer 2xx.
Node.js (Express)
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const SECRET = process.env.SENDALTO_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
const app = express();

// Verify against the raw bytes: parsing and re-serialising JSON changes the body.
app.post('/sendalto-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const header = req.get('x-sendalto-signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map(part => part.trim().split('=', 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
    return res.sendStatus(400); // stale or missing timestamp
  }
  const expected = createHmac('sha256', SECRET).update(`${parts.t}.`).update(req.body).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
    return res.sendStatus(401); // not signed with your secret
  }

  // Delivery is at least once: remember each x-sendalto-delivery and skip repeats.
  const deliveryId = req.get('x-sendalto-delivery');
  if (await alreadyProcessed(deliveryId)) return res.sendStatus(200);

  const event = JSON.parse(req.body.toString('utf8'));
  await handleEvent(event); // event.id, event.type, event.occurredAt, event.data.email, event.data.properties
  await markProcessed(deliveryId);
  res.sendStatus(200);
});

4. Retries

  • Any 2xx within 10 seconds counts as delivered. Answer quickly and do slow work afterwards.
  • Timeouts, network errors, 408, 425, 429 and 5xx are retried with growing pauses — about 1, 2, 4, 8, 16, 32 and 64 minutes — up to eight attempts in total.
  • Other 4xx answers stop retries at once, so return 4xx only for requests you will never accept.
  • Delivery is at least once: a retry can arrive after you already processed the event, including after a Sendalto worker restart. Deduplicate by x-sendalto-delivery.
  • Failed deliveries are listed in Settings → API & privacy, where you can retry them. You can pause a subscription while you move your receiver.

5. Rotate the secret

Rotating shows a new secret once; it signs every following attempt, including retries of earlier events. Deploy the new secret to your receiver at the same moment, or pause the subscription during the switch.