Agent skill

Brevo Webhooks Skill

Receive and authenticate Brevo (formerly Sendinblue) webhooks. Use when setting up Brevo webhook handlers, securing them with Basic auth or a bearer token (Brevo has no payload signature), or handling transactional and marketing email events like delivered, opened, click, hard_bounce, soft_bounce, spam, and unsubscribed.

Install this skill

npx skills add hookdeck/webhook-skills --skill brevo-webhooks


Brevo (formerly Sendinblue) POSTs JSON to your notify URL when transactional or marketing emails are sent, delivered, opened, clicked, bounced, or marked as spam, and when contacts change.

When to Use This Skill

  • How do I receive Brevo (Sendinblue) webhooks?
  • How do I verify Brevo webhooks? Is there a Brevo webhook signature?
  • How do I secure a Brevo webhook with Basic auth or a bearer token?
  • How do I handle Brevo delivered, hard_bounce, spam, or unsubscribed events?
  • Why does my Brevo webhook say hard_bounce when I subscribed to hardBounce?

Verification (core)

Brevo does not sign webhook payloads. There is no signature header and no HMAC to compute. You secure the webhook when you create it, with one of:

MethodHow you configure itWhat Brevo sends
Basic authCredentials in the notify URL: https://user:pass@host/webhooks/brevoAuthorization: Basic base64(user:pass)
Bearer token"auth": {"type": "bearer", "token": "..."} on create/update webhookAuthorization: Bearer <token>
Custom headers"headers": [{"key": "...", "value": "..."}] on create/update webhookYour headers, verbatim

Add Brevo's IP allowlist at your firewall as an extra layer. Compare credentials timing-safe. Node:

const crypto = require('crypto');
const safeEqual = (a, b) => {
  const ab = Buffer.from(a), bb = Buffer.from(b);
  return ab.length === bb.length && crypto.timingSafeEqual(ab, bb);
};

function verifyBrevoAuth(authHeader, { username, password, token } = {}) {
  if (!authHeader) return false;
  if (token && authHeader.startsWith('Bearer ')) return safeEqual(authHeader.slice(7), token);
  if (username && password && authHeader.startsWith('Basic ')) {
    const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
    const i = decoded.indexOf(':'); // passwords may contain ':'
    if (i === -1) return false;
    const userOk = safeEqual(decoded.slice(0, i), username);
    const passOk = safeEqual(decoded.slice(i + 1), password);
    return userOk && passOk;
  }
  return false;
}

No body signature means the raw body isn't security-critical: normal JSON parsing is fine. With batched webhooks ("batched": true) the body is an array of events, so normalize to an array before dispatching.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

Dispatch on the payload's event field. These are snake_case and differ from the camelCase names you subscribe with (hardBounce → hard_bounce, uniqueOpened → unique_opened, sent → request).

event valueTypeTriggered when
requestTransactionalEmail sent ("Sent")
deliveredBothEmail accepted by the recipient's server
opened / unique_openedOpened: both; first opening: transactionalEmail opened / opened for the first time
proxy_open / unique_proxy_openproxy_open: bothOpened via a privacy proxy (e.g. Apple MPP)
clickBothLink clicked (link field in transactional, URL in marketing)
soft_bounce / hard_bounceBothTemporary / permanent delivery failure
spamBothRecipient marked the email as spam
unsubscribedTransactionalRecipient unsubscribed
unsubscribeMarketingRecipient unsubscribed from a campaign
blocked, invalid_email, deferred, errorTransactionalNot sent, invalid address, delayed, or failed
list_addition, contact_updated, contact_deletedMarketing (contacts)Contact added to a list, updated, or deleted

SMS webhooks ("channel": "sms") have no event field; they carry msg_status instead. See references/overview.md.

Environment Variables

Set the variables for the method you configured on the webhook:

BREVO_WEBHOOK_USERNAME=your_webhook_username   # Basic auth (URL credentials)
BREVO_WEBHOOK_PASSWORD=your_webhook_password
BREVO_WEBHOOK_TOKEN=your_webhook_token         # Bearer token (auth.type = bearer)

The examples fail closed (return 500) if neither is configured.

Local Development

For local webhook testing, run the Hookdeck CLI via npx (no install required):

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

No account required. The CLI provides a public URL that tunnels to your local server, plus a web UI for inspecting requests.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →