Webhooks

Configure webhook endpoints to receive real-time event notifications with HMAC signing.

Endpoints

MethodPathPermission
GET
webhooks.read
POST
webhooks.write
PATCH
webhooks.write
DELETE
webhooks.write
GET
webhooks.read

Webhook Headers

Every webhook request includes the following headers

Content-Type: application/json
X-Sendy-Signature: sha256=<hmac_hex_digest>
X-Sendy-Event: email.sent
X-Sendy-Delivery: <delivery_uuid>
http

Event Types

Subscribe to specific events when creating a webhook. Click an event to see its payload.

Email Events

SMTP Events

SMS Events

WhatsApp Events

Payload field naming is not uniform across these events — whatsapp.sent and whatsapp.failed use snake_case, the others camelCase. Each example below mirrors what is actually dispatched.

IMAP Events

Warmup Events

AI Events

Correlating events with your own records

Pass metadata at send time; every email.* event carries it back.

Rather than storing Sendy's email id against your own records, attach your identifiers to the send and read them straight off the event.metadata is a flat object of strings, numbers or booleans — at most 20 keys and 8 KB — returned unchanged in data.metadata, or null if you sent none.

curl -X POST https://sendy.cloud/api/v1/emails/send \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "subject": "Invoice INV-2026-0042",
    "html": "<p>Your invoice is ready.</p>",
    "metadata": { "invoice_id": "INV-2026-0042", "reminder": 1 }
  }'
bash

Signature Verification

Verify webhook authenticity using the HMAC-SHA256 signature

Every webhook request includes an X-Sendy-Signature header containing an HMAC-SHA256 digest of the request body, signed with your webhook secret. Always verify this signature before processing events.

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-sendy-signature'];
  const isValid = verifyWebhook(
    JSON.stringify(req.body),
    signature,
    'your_webhook_secret'
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event...
  console.log(req.body.event); // "email.sent"
  res.status(200).send('OK');
});
javascript