Webhooks
Subscribe to push delivery and engagement events so your systems update order status, prune bad tokens, and measure open rates without polling. Configure a single HTTPS endpoint and filter by event type.
Real-time delivery
Events POST to your HTTPS endpoint shortly after FCM/APNs or the client reports status.
HMAC signatures
Every payload is signed so you can reject spoofed or replayed requests.
Automatic retries
Failed deliveries (non-2xx) are retried with exponential backoff for up to 24 hours.
Opens included
Track engagement with push.opened when your app reports notification taps.
Authentication
Authorization: Basic <token>. Auth guide →Configure in the dashboard
push.delivered, push.failed, and push.opened. See also Webhook concepts.Always verify signatures
X-Sendexa-Signature. Unverified endpoints are a security risk.<500ms
event → first attempt
5+
exponential backoff
3
delivered · failed · opened
SHA-256
HMAC hex
The platform confirmed the notification was delivered to the device (when available).
Permanent or terminal failure — invalid token, credentials, or provider rejection.
The user opened/tapped the notification (reported by your client or SDK integration).
Common envelope
All Mobile Push webhooks share the same outer structure. Event-specific fields live under data.
{"id": "evt_01J8PUSH0000000000000001","type": "push.delivered","apiVersion": "v1","createdAt": "2026-07-12T11:20:05.120Z","businessId": "BU-VOIF86X7","data": { }}
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Unique event id. Use for idempotent processing (dedupe). |
| type | string | required | push.delivered | push.failed | push.opened |
| apiVersion | string | required | Envelope version (currently v1). |
| createdAt | string (ISO 8601) | required | When the event was generated by Sendexa. |
| businessId | string | required | Your business identifier. |
| data | object | required | Event payload (see below). |
push.deliveredFired when Sendexa receives confirmation that the notification was delivered to a device. Availability of delivery receipts depends on the platform and OS version; some deliveries may only reach sent without a later delivered event.
{"id": "evt_01J8PUSHDELIVERED000001","type": "push.delivered","apiVersion": "v1","createdAt": "2026-07-12T11:20:05.120Z","businessId": "BU-VOIF86X7","data": {"pushId": "PUSH-8c1a2b3d-4e5f-6789-abcd-ef0123456789","status": "delivered","channel": "push","target": {"type": "device","value": "dG9rZW4tZXhhbXBsZS1mY20...","platform": "android","userId": "user_abc123"},"notification": {"title": "Payment received","body": "GHS 250.00 was credited to your wallet."},"provider": {"name": "fcm","messageId": "projects/my-app/messages/0:1710000000000000%..."},"deliveredAt": "2026-07-12T11:20:04.980Z","submittedAt": "2026-07-12T11:20:00.000Z"}}
| Field | Type | Required | Description |
|---|---|---|---|
| data.pushId | string | required | Matches the pushId from POST /v1/push/send. |
| data.status | string | required | Always "delivered" for this event. |
| data.target | object | required | Resolved device target including platform and optional userId. |
| data.provider | object | optional | FCM or APNs provider identifiers when available. |
| data.deliveredAt | string | required | ISO timestamp of delivery confirmation. |
push.failedFired when delivery fails terminally for a device. Use error.code to decide whether to delete the token.
{"id": "evt_01J8PUSHFAILED000000002","type": "push.failed","apiVersion": "v1","createdAt": "2026-07-12T11:20:06.200Z","businessId": "BU-VOIF86X7","data": {"pushId": "PUSH-8c1a2b3d-4e5f-6789-abcd-ef0123456789","status": "failed","channel": "push","target": {"type": "device","value": "dG9rZW4tZXhwaXJlZC10b2tlbg...","platform": "android","userId": "user_abc123"},"error": {"code": "UNREGISTERED","message": "Device token is no longer valid.","providerCode": "messaging/registration-token-not-registered","retryable": false},"failedAt": "2026-07-12T11:20:06.100Z","submittedAt": "2026-07-12T11:20:00.000Z"}}
| error.code | retryable | Recommended action |
|---|---|---|
UNREGISTERED | false | DELETE /v1/push/devices/:token |
INVALID_TOKEN | false | Delete token; fix registration flow |
AUTH_ERROR | false | Check FCM/APNs credentials in dashboard |
PROVIDER_UNAVAILABLE | true | Sendexa may already have retried; alert if spike |
PAYLOAD_REJECTED | false | Reduce payload size; fix image URL |
EXPIRED | false | TTL elapsed offline; resend if still relevant |
push.openedFired when the user opens the notification. Your mobile app (or an official Sendexa helper SDK, when available) should report the open so Sendexa can emit this event. Correlate with pushId from the original send response (pass it through data if needed).
{"id": "evt_01J8PUSHOPENED000000003","type": "push.opened","apiVersion": "v1","createdAt": "2026-07-12T11:22:10.000Z","businessId": "BU-VOIF86X7","data": {"pushId": "PUSH-8c1a2b3d-4e5f-6789-abcd-ef0123456789","status": "opened","channel": "push","target": {"type": "device","value": "dG9rZW4tZXhhbXBsZS1mY20...","platform": "android","userId": "user_abc123"},"openedAt": "2026-07-12T11:22:09.500Z","submittedAt": "2026-07-12T11:20:00.000Z","context": {"os": "android","appVersion": "3.2.1"}}}
Open tracking note
push.opened remains accurate.| Header | Description |
|---|---|
| Content-Type | application/json |
| X-Sendexa-Signature | Hex-encoded HMAC-SHA256 of the raw body using your webhook secret |
| X-Sendexa-Event | Same as body type (e.g. push.delivered) |
| X-Sendexa-Delivery | Unique delivery attempt id |
| User-Agent | Sendexa-Webhooks/1.0 |
Verify HMAC (Node.js)
import crypto from 'crypto';import express from 'express';const app = express();// IMPORTANT: use raw body for signature verificationapp.post('/webhooks/sendexa/push',express.raw({ type: 'application/json' }),(req, res) => {const secret = process.env.SENDEXA_WEBHOOK_SECRET;const signature = req.get('X-Sendexa-Signature') || '';const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');const a = Buffer.from(signature);const b = Buffer.from(expected);if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {return res.status(401).send('invalid signature');}const event = JSON.parse(req.body.toString('utf8'));// Idempotency: skip if event.id already processedswitch (event.type) {case 'push.delivered':// mark message delivered in DBbreak;case 'push.failed':if (['UNREGISTERED', 'INVALID_TOKEN'].includes(event.data?.error?.code)) {// DELETE /v1/push/devices/:token}break;case 'push.opened':// analytics / attributionbreak;}res.status(200).json({ received: true });});
Verify HMAC (Python)
import hmacimport hashlibfrom flask import Flask, request, abortapp = Flask(__name__)@app.post("/webhooks/sendexa/push")def push_webhook():secret = b"YOUR_WEBHOOK_SECRET"signature = request.headers.get("X-Sendexa-Signature", "")raw = request.get_data()expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()if not hmac.compare_digest(signature, expected):abort(401)event = request.get_json(force=True)# process event["type"], event["id"], event["data"]return {"received": True}, 200
Your endpoint should respond with 2xx within 10 seconds. Any other status (or timeout) schedules a retry:
| Attempt | Delay after previous |
|---|---|
| 1 | Immediate |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6+ | Continuing backoff up to 24h total window |
- Respond quickly; process heavy work asynchronously after returning 200.
- Use
event.idfor idempotency — retries may deliver the same event more than once. - Delivery attempt id is in
X-Sendexa-Delivery.
When you send with type: user or type: topic, a single API call may produce multiple webhook events — one per resolved device — all sharing the same pushId but different target tokens. Aggregate on pushId for campaign-level metrics, or on token for per-device state.
- HTTPS only; valid public certificate (no self-signed in production).
- Return 2xx only after signature verification and durable enqueue/store.
- On
push.failedwith non-retryable token errors, call DELETE /v1/push/devices/:token. - Store
pushIdwhen sending so support can trace a user complaint to webhook history. - Monitor webhook success rate in Dashboard → Webhooks → Logs.
Best practices
- Prefer webhooks over polling for all delivery and open analytics.
- Keep handlers idempotent using
event.idand/orpushId + token + type. - Separate secrets per environment (sandbox vs production).
- Log failed signature attempts — they may indicate misconfiguration or an attack.
- For high volume topic campaigns, scale your consumer with a queue behind the HTTP endpoint.