Receive signed webhooks
React to incoming messages and account events without repeatedly calling the API.
Create a subscription
Use Integrations or POST /webhooks/ with an owner or admin credential that has webhook permission. Email events also require read permission, selected mailboxes, and current mailbox grants. Select the exact events and mailbox IDs you need. Save the signing secret when it is shown.
curl -X POST 'https://cherrymail.net/api/v1/webhooks/' \
-H "Authorization: Bearer $CHERRYMAIL_API_KEY" \
-H "Idempotency-Key: example-request-1042" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example/cherrymail/events",
"events": [
"message.received"
],
"mailbox_ids": [
123
]
}'Destinations must use public HTTPS on port 443. Redirects, private addresses, and internal networks are refused. Destination DNS is revalidated when connecting.
Available events
| Event | Purpose |
|---|---|
message.received | Mailbox activity |
message.submitted | Mailbox activity |
domain.verified | Resource or service status |
mailbox.ready | Resource or service status |
mailbox.deleted | Resource or service status |
alias.ready | Resource or service status |
alias.deleted | Resource or service status |
storage.80 | Storage threshold |
storage.100 | Storage threshold |
service.suspended | Resource or service status |
service.ready | Resource or service status |
webhook.test | Labelled test delivery |
Email events contain identifiers and minimal metadata. Retrieve message content and attachments through the authorized API.
Verify every delivery
Read the raw request body before parsing JSON. Compute HMAC-SHA256 over timestamp + "." + rawBody with your signing secret. Compare it to CherryMail-Signature, including its v1= prefix.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, timestamp, signature, secret) {
if (!/^\d+$/.test(timestamp) ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const expected = Buffer.from('v1=' + createHmac('sha256', secret)
.update(timestamp + '.').update(rawBody).digest('hex'));
const actual = Buffer.from(signature);
return actual.length === expected.length &&
timingSafeEqual(actual, expected);
}Use the CherryMail-Timestamp header, reject timestamps outside a five-minute window, compare signatures in constant time, and deduplicate by the stable event ID.
Delivery, retries & replay
Return a 2xx response promptly, then process the event asynchronously. Delivery is at least once and ordering is not guaranteed. Up to ten automatic attempts use exponential backoff.
Inspect delivery history, send test events, and manually replay deliveries in Integrations. A replay preserves the original event ID, so your deduplication must also handle intentional replays. Revoking a credential or grant stops dependent deliveries.