Webhooks
Webhooks let your app react to email lifecycle events as they happen. Send → Delivery → Open → Click → Bounce → Complaint → Reply — every state transition fires an HTTP POST to your URL with a structured JSON payload.
Two webhook surfaces
Pick whichever fits your integration:
| Surface | Where you set it | Scope | Best for |
|---|---|---|---|
| Account-level webhook | Settings → Webhook URL on the partner panel | Every event for every send + lifecycle events (production-access approval, account deletion, etc) | Single endpoint that handles everything; simplest setup |
| Per-config-set destinations | POST /partner/email/configuration-sets/:id/event-destinations | Only events scoped to that config set, filtered by matching_event_types | Per-customer or per-product routing; advanced filtering |
You can use both at once. The account-level URL receives every event regardless of config set; the per-config-set destinations receive their filtered subset additionally. Most partners start with just the account-level URL and add per-config-set destinations later when they need to route per-customer.
Event types fired to your webhook
| Event | When | Surface |
|---|---|---|
email.sent | Recipient MX accepted the message (250 OK) | both |
email.delivered | Same as sent — direct-MX collapses these | both |
email.bounced | Hard or soft bounce reported via DSN | both |
email.complained | Recipient marked as spam (FBL) | both |
email.opened | Recipient opened the email (1×1 pixel loaded) | both |
email.clicked | Recipient clicked a link in the email | both |
email.rejected | Send refused at the API layer (suppression / quota) | both |
email.replied | Recipient replied to the email | both |
production_access.approved | Admin lifted your sandbox cap | account-level only |
production_access.denied | Admin denied your sandbox lift request | account-level only |
account.deletion.requested | You requested account deletion | account-level only |
account.deletion.cancelled | You cancelled the deletion within the 48h window | account-level only |
rcs_kyc.approved | Admin approved a customer’s RCS KYC | account-level only |
rcs_kyc.rejected | Admin rejected a customer’s RCS KYC (see data.rejection_reason) | account-level only |
rcs.message.text | End-user replied with text — see RCS Incoming | account-level only |
rcs.message.media | End-user sent media (image / video / PDF) | account-level only |
rcs.message.location | End-user shared a location | account-level only |
rcs.suggestion.response | End-user tapped a reply / URL / dial / calendar suggestion | account-level only |
rcs.status.sent | Message you sent via send-rcs was accepted by the carrier | account-level only |
rcs.status.delivered | Delivered to the recipient’s device | account-level only |
rcs.status.read | Recipient opened the message | account-level only |
rcs.status.failed | Carrier-side failure — see data.rm_event.failure_reason | account-level only |
WABA_ONBOARDED | Customer completed WhatsApp embedded signup — see WhatsApp Events | account-level only |
WhatsApp sent / delivered / read / failed / deleted | Outbound message status — raw Meta envelope, forwarded as-is | account-level only |
WhatsApp inbound (text / image / video / audio / document / sticker / location / contacts / interactive / button / order / reaction / system / unknown) | Customer-initiated messages — raw Meta envelope, forwarded as-is | account-level only |
How it works
- You create a configuration set (a logical grouping for sends).
- You add a webhook event destination to that config set, with the URL + secret + the event types you want.
- You send emails with
configuration_set_namereferencing that set. - Every event for those sends gets POSTed to your URL with an HMAC-signed body.
- Retries: 5xx + timeout → 1min, 5min, 15min. 4xx → no retry (your URL is broken; we don’t waste retries).
Set up a webhook in 3 calls
1. Create a config set
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"suppression_options": "BOUNCE_AND_COMPLAINT"
}'Save the config_set_id from the response.
2. Add a webhook destination
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets/$CONFIG_SET_ID/event-destinations \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-webhook",
"destination_type": "WEBHOOK",
"webhook_url": "https://yourapp.com/webhooks/splashify",
"webhook_secret": "your-shared-secret-32-chars",
"matching_event_types": ["send", "delivered", "bounce", "complaint", "open", "click"]
}'Security: Store the
webhook_secretsomewhere your endpoint can read it (env var). The API will never echo it back — to rotate, PATCH a new value.
3. Reference the config set on send
curl https://api.splashifypro.com/api/v1/partner/email/send \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Welcome",
"html_body": "<h1>Hi</h1>",
"configuration_set_name": "production"
}'Within seconds your endpoint receives a Send event, then
Delivery, etc.
Payload shape
We follow the AWS SES event-publishing JSON envelope. Code written against AWS SES SNS subscriptions consumes our webhooks by swapping the auth header verification.
{
"eventType": "Delivery",
"mail": {
"timestamp": "2026-05-03T12:34:56Z",
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"source": "[email protected]",
"destination": ["[email protected]"]
},
"delivery": {
"timestamp": "2026-05-03T12:34:57Z",
"recipients": ["[email protected]"],
"smtpResponse": "250 OK"
}
}Every event has the top-level eventType and mail fields. The
event-specific payload is nested under a key matching the lowercase
event type — delivery, bounce, complaint, etc.
Headers
Every POST carries:
| Header | Purpose |
|---|---|
Content-Type | application/json |
User-Agent | Splashify-Pro-Webhook/1.0 |
X-Splashify-Event | Event type — Send, Delivery, Bounce, … |
X-Splashify-Signature | sha256=<hex> HMAC-SHA256 of the raw body keyed by your webhook_secret |
X-Splashify-Timestamp | Unix seconds — protects against replay |
X-Splashify-Delivery-ID | UUID per delivery attempt — use for idempotency |
Quick verify in Node
import crypto from "crypto";
app.post("/webhooks/splashify", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("x-splashify-signature") || "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.SPLASHIFY_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// process event...
res.status(200).end();
});Next
- Event Types → — full payload shape per event
- Verify Signature → — implementations in 6 languages
- Retries & Replays → — backoff schedule + manual replay
- Best Practices → — idempotency, dedup, security