Webhooks
A signed POST to your endpoint whenever something happens to a message - delivered, bounced, marked as spam, opened or clicked - plus inbound mail as JSON.
Setting up
Webhooks are set per hive. In the dashboard, open a hive, go to Webhooks, add your endpoint URL and tick the events you want. Each webhook gets its own signing secret, shown on that page - use it to verify every request. You can add several webhooks to one hive (for example one per environment), pause them, and see recent deliveries.
- The URL must be reachable from the internet. Addresses on private or internal networks are refused.
- Inbound mail uses a separate setting - the hive’s inbound webhook URL. See Inbound mail.
The request
Every event is a POST with a JSON body and these headers:
Headers
Content-Type
application/jsonX-Buzzmark-Event
delivery, bounce, spam_complaint, open or click.X-Buzzmark-Signature
The body is described per event below. Use MessageID to tie an event
to the message you sent, and Metadata to tie it to your own records.
POST /webhooks/buzzmark HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-Buzzmark-Event: delivery
X-Buzzmark-Signature: 5d41402abc4b2a76b9719d911017c592...
{
"RecordType": "Delivery",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "customer@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-24T10:15:06+00:00"
}
Verifying signatures
Before trusting a request, compute the HMAC-SHA256 of the raw body - the exact bytes you
received, before any JSON parsing - with your webhook’s signing secret, and compare it with
X-Buzzmark-Signature using a constant-time comparison. Reject the request if they don’t match.
- Parsing and re-encoding the JSON changes the bytes and breaks the signature - always hash the raw body.
- Each webhook has its own secret. If you have several, use the right one for each endpoint.
// Laravel: verify before trusting the payload
$signature = (string) $request->header('X-Buzzmark-Signature');
$expected = hash_hmac('sha256', $request->getContent(), $webhookSecret);
if (! hash_equals($expected, $signature)) {
abort(401);
}
$event = $request->json()->all();
// Express: keep the raw body for the signature
app.post('/webhooks/buzzmark', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.BUZZMARK_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const given = req.get('X-Buzzmark-Signature') || '';
if (given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200);
});
# Flask
import hashlib, hmac
@app.post("/webhooks/buzzmark")
def buzzmark_webhook():
expected = hmac.new(SECRET.encode(), request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Buzzmark-Signature", "")):
abort(401)
event = request.get_json()
return "", 200
Responses & retries
Answer with any 2xx status as soon as you’ve stored the event - do slow work afterwards. Anything else counts as a failure:
Responses
- 2xx Delivered. Nothing more happens.
- 3xx Redirects aren’t followed, so a redirect counts as a failure. Use the final URL.
- 4xx / 5xx Failed, and retried - as is no answer within 15 seconds.
A failed delivery is retried up to 6 attempts in total, waiting 10s, 30s, 2m, 5m and 15m between them. After the last attempt it’s marked failed. The hive’s Webhooks page lists recent deliveries with the last status code your endpoint returned and the number of attempts, and you can send a failed delivery again from there.
MessageID + RecordType + ReceivedAt (plus OriginalLink for clicks)
as the key.
Events
Each event is sent to every webhook on the message’s hive that subscribes to it.
delivery
your webhook URL · X-Buzzmark-Event: delivery
Auth: X-Buzzmark-Signature
The recipient’s mail server accepted the message. That’s as far as delivery can be confirmed - it doesn’t mean it reached the inbox rather than spam.
Payload
RecordType
Delivery, Bounce, SpamComplaint, Open or Click.MessageID
Recipient
Tag
Tag you sent the message with.Metadata
Metadata you sent the message with - use it to find the record in your own system.ReceivedAt
{
"RecordType": "Delivery",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "customer@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-24T10:15:06+00:00"
}
bounce
your webhook URL · X-Buzzmark-Event: bounce
Auth: X-Buzzmark-Signature
The recipient’s server refused the message, straight away or later with a bounce report. The message’s
status becomes bounced. A hard bounce (the address doesn’t exist) also adds the
recipient to the hive’s suppression list, so future sends to it are refused; a soft bounce
(mailbox full, server busy) doesn’t.
Payload
RecordType
Delivery, Bounce, SpamComplaint, Open or Click.MessageID
Recipient
Tag
Tag you sent the message with.Metadata
Metadata you sent the message with - use it to find the record in your own system.ReceivedAt
Type
HardBounce or SoftBounce.Description
{
"RecordType": "Bounce",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "gone@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-24T10:15:09+00:00",
"Type": "HardBounce",
"Description": "550 5.1.1 The email account that you tried to reach does not exist"
}
spam_complaint
your webhook URL · X-Buzzmark-Event: spam_complaint
Auth: X-Buzzmark-Signature
The recipient marked the message as spam, reported back by their mailbox provider. The recipient is added to the hive’s suppression list straight away - keep complaints low, as they hurt your sending reputation.
Payload
RecordType
Delivery, Bounce, SpamComplaint, Open or Click.MessageID
Recipient
Tag
Tag you sent the message with.Metadata
Metadata you sent the message with - use it to find the record in your own system.ReceivedAt
{
"RecordType": "SpamComplaint",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "customer@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-25T08:02:41+00:00"
}
open
your webhook URL · X-Buzzmark-Event: open
Auth: X-Buzzmark-Signature
The recipient opened the message: a tiny tracking image in the HTML body loaded. Needs open tracking turned on for the hive and an HTML body. Sent for every open, not just the first. Some mail apps load images automatically or block them, so treat opens as a signal rather than proof.
Payload
RecordType
Delivery, Bounce, SpamComplaint, Open or Click.MessageID
Recipient
Tag
Tag you sent the message with.Metadata
Metadata you sent the message with - use it to find the record in your own system.ReceivedAt
{
"RecordType": "Open",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "customer@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-24T10:21:40+00:00"
}
click
your webhook URL · X-Buzzmark-Event: click
Auth: X-Buzzmark-Signature
The recipient clicked a link. Needs link tracking turned on for the hive - Buzzmark rewrites links in the HTML and/or text body, depending on the setting, records the click and sends the reader on to the original address.
Payload
RecordType
Delivery, Bounce, SpamComplaint, Open or Click.MessageID
Recipient
Tag
Tag you sent the message with.Metadata
Metadata you sent the message with - use it to find the record in your own system.ReceivedAt
OriginalLink
{
"RecordType": "Click",
"MessageID": "f5ab446c-18ad-4db4-b747-8e1ba70ec72d",
"Recipient": "customer@example.com",
"Tag": "receipt",
"Metadata": { "order_id": "1234" },
"ReceivedAt": "2026-10-24T10:22:03+00:00",
"OriginalLink": "https://acme.com/orders/1234"
}
Inbound mail
Inbound mail
the hive's inbound webhook URL
Auth: X-Buzzmark-Inbound-Hash
Mail sent to the hive’s address ({hash}@in.buzzmarkapp.com) or to a domain you’ve
set up for receiving is parsed and posted to the hive’s inbound webhook URL. See
Inbound email for addresses, MX setup and forwarding.
-
Authenticate by checking the
X-Buzzmark-Inbound-Hashheader equals your hive’s inbound hash (the part before the@in its address). Inbound requests aren’t HMAC-signed. -
Formats: the native Buzzmark JSON below, or - to reuse an existing handler - Postmark’s
JSON, or SendGrid’s or Mailgun’s form fields (sent as
application/x-www-form-urlencoded). Choose it in the hive’s settings. -
Retries: answer
2xxwithin 15 seconds. Otherwise it’s retried up to 5 attempts, waiting 10s, 30s, 2m and 5m. The message stays in the hive’s Inbound log either way, where you can forward it again.
Payload (Buzzmark format)
RecordType
Inbound.MessageID
From
From: header (may include a name).FromName
To
To: header - the address the sender wrote.OriginalRecipient
To when a mailbox provider forwarded it to you, or it was Bcc’d.Subject
HtmlBody
TextBody
Headers
X- headers, as [{"Name", "Value"}].Attachments
Name, ContentType, ContentLength (bytes). File contents aren’t included.ReceivedAt
{
"RecordType": "Inbound",
"MessageID": "9d1e7c3a-2b4f-4c1a-8e0d-5f6a7b8c9d0e",
"From": "Maria Lopez <maria@cliente.es>",
"FromName": null,
"To": "support@acme.com",
"OriginalRecipient": "support@acme.com",
"Subject": "Where is my order?",
"HtmlBody": "<p>Hi, order 1234 hasn't arrived yet.</p>",
"TextBody": "Hi, order 1234 hasn't arrived yet.",
"Headers": [{ "Name": "X-Mailer", "Value": "Apple Mail" }],
"Attachments": [
{ "Name": "photo.jpg", "ContentType": "image/jpeg", "ContentLength": 48213 }
],
"ReceivedAt": "2026-10-24T11:04:17+00:00"
}
Next
- API reference - send mail and read your message log.
- Inbound email - receive on your own domain, formats and forwarding.
- SMTP - send through a mail relay.