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, orunsubscribedevents? - Why does my Brevo webhook say
hard_bouncewhen I subscribed tohardBounce?
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:
| Method | How you configure it | What Brevo sends |
|---|---|---|
| Basic auth | Credentials in the notify URL: https://user:pass@host/webhooks/brevo | Authorization: Basic base64(user:pass) |
| Bearer token | "auth": {"type": "bearer", "token": "..."} on create/update webhook | Authorization: Bearer <token> |
| Custom headers | "headers": [{"key": "...", "value": "..."}] on create/update webhook | Your 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 value | Type | Triggered when |
|---|---|---|
request | Transactional | Email sent ("Sent") |
delivered | Both | Email accepted by the recipient's server |
opened / unique_opened | Opened: both; first opening: transactional | Email opened / opened for the first time |
proxy_open / unique_proxy_open | proxy_open: both | Opened via a privacy proxy (e.g. Apple MPP) |
click | Both | Link clicked (link field in transactional, URL in marketing) |
soft_bounce / hard_bounce | Both | Temporary / permanent delivery failure |
spam | Both | Recipient marked the email as spam |
unsubscribed | Transactional | Recipient unsubscribed |
unsubscribe | Marketing | Recipient unsubscribed from a campaign |
blocked, invalid_email, deferred, error | Transactional | Not sent, invalid address, delayed, or failed |
list_addition, contact_updated, contact_deleted | Marketing (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
- references/overview.md - Brevo webhook concepts, event names, payload fields
- references/setup.md - Create webhooks in the dashboard or API with auth
- references/verification.md - Basic auth, bearer token, custom headers, IP allowlist
- examples/ - Express, Next.js, and FastAPI handlers with tests