Webhook Best Practices
1. Verify the signature on every request
Constant-time HMAC compare. See Verify Signature for per-language code.
2. Ack fast, process async
Your endpoint has 10 seconds before we time out + retry. The retry DOES NOT mean the first call failed — we have no idea until we get a response. Best pattern:
app.post("/webhooks/splashify", express.raw({...}), async (req, res) => {
// 1. Verify signature
// 2. Push to internal queue
await queue.push({ rawBody: req.body.toString(), receivedAt: Date.now() });
// 3. Ack immediately
res.status(200).end();
});Process the queue with retries inside your own infrastructure. Your endpoint becomes a fast Layer-7 doorman.
3. Dedup by mail.messageId + eventType
Retried deliveries have the same logical content but different
X-Splashify-Delivery-ID headers. Use the body fields as your dedup
key:
const key = `${body.mail.messageId}:${body.eventType}`;4. Handle out-of-order delivery
The dispatcher fans out one POST per (event, destination) combo
in parallel — events for the same email may arrive in any order.
A Bounce event might land before the Send event for the same
messageId if the bounce was inline.
Don’t assume Send → Delivery → Open → Click arrive in order.
Drive your state machine off the absolute event types, not their
sequence.
5. Monitor your endpoint’s health
Webhook delivery metrics are available via:
curl 'https://apis.splashifypro.com/api/v1/partner/email/events?day_bucket=$(date -u +%Y-%m-%d)' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"Track:
- Delivery rate — % of events delivered to your endpoint
- Average latency —
delivered_at - created_at - Failed events — events with
status=failedorgave_up
A sudden drop in delivery rate usually means your endpoint started returning 5xx — check your app logs.
6. Don’t expose your webhook URL publicly
The HMAC signature stops forged events, but exposing the URL still attracts unwanted traffic (probes, fuzzing). Guard at the network edge:
- Only allow
POST(not GET / OPTIONS) - Whitelist our outbound IP range if your firewall supports it. IPs are published at status.splashifypro.com/ip-ranges and updated when new sending capacity is added — subscribe to the changelog so you don’t miss additions.
- Reject any request without
X-Splashify-Signature
7. Use one webhook per concern
Don’t have one giant webhook handler that branches on eventType.
Have separate config-set destinations for distinct concerns:
config_set "production-engagement" → /webhooks/engagement
→ matching_event_types: ["open", "click"]
config_set "production-deliverability" → /webhooks/deliverability
→ matching_event_types: ["bounce", "complaint", "reject"]
config_set "production-archive" → /webhooks/archive
→ matching_event_types: ["send", "delivered", "bounce", "complaint", "open", "click", "reject"]Smaller handlers = easier to reason about + scope failures.
8. Log raw payloads for the first 30 days
Keep raw event bodies + headers in your logs (with the webhook_secret
redacted). When something looks weird, you have the original payload
to diff against your handler’s behaviour.
9. Replay before going to production
Use a tool like Webhook.site to point test sends at — verify your signature check, JSON parsing, and event-type dispatch logic before pointing your real endpoint at us.
10. Have a fallback
Webhooks can drop. If your business depends on knowing whether an
email delivered, periodically poll /emails/:message_id for any
message you sent in the last hour that hasn’t reached a terminal
state via webhook. Reconcile the difference.