Mobile Push API
Beta
REST

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.

Latency

<500ms

event → first attempt

Max retries

5+

exponential backoff

Event types

3

delivered · failed · opened

Signature

SHA-256

HMAC hex

Event types
push.delivered

The platform confirmed the notification was delivered to the device (when available).

push.failed

Permanent or terminal failure — invalid token, credentials, or provider rejection.

push.opened

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.

JSON
{
"id": "evt_01J8PUSH0000000000000001",
"type": "push.delivered",
"apiVersion": "v1",
"createdAt": "2026-07-12T11:20:05.120Z",
"businessId": "BU-VOIF86X7",
"data": { }
}
FieldTypeRequiredDescription
idstring
required
Unique event id. Use for idempotent processing (dedupe).
typestring
required
push.delivered | push.failed | push.opened
apiVersionstring
required
Envelope version (currently v1).
createdAtstring (ISO 8601)
required
When the event was generated by Sendexa.
businessIdstring
required
Your business identifier.
dataobject
required
Event payload (see below).
push.delivered

Fired 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.

JSON
{
"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"
}
}
FieldTypeRequiredDescription
data.pushIdstring
required
Matches the pushId from POST /v1/push/send.
data.statusstring
required
Always "delivered" for this event.
data.targetobject
required
Resolved device target including platform and optional userId.
data.providerobjectoptionalFCM or APNs provider identifiers when available.
data.deliveredAtstring
required
ISO timestamp of delivery confirmation.
push.failed

Fired when delivery fails terminally for a device. Use error.code to decide whether to delete the token.

JSON
{
"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.coderetryableRecommended action
UNREGISTEREDfalseDELETE /v1/push/devices/:token
INVALID_TOKENfalseDelete token; fix registration flow
AUTH_ERRORfalseCheck FCM/APNs credentials in dashboard
PROVIDER_UNAVAILABLEtrueSendexa may already have retried; alert if spike
PAYLOAD_REJECTEDfalseReduce payload size; fix image URL
EXPIREDfalseTTL elapsed offline; resend if still relevant
push.opened

Fired 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).

JSON
{
"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"
}
}
}
Request headers & signature verification
HeaderDescription
Content-Typeapplication/json
X-Sendexa-SignatureHex-encoded HMAC-SHA256 of the raw body using your webhook secret
X-Sendexa-EventSame as body type (e.g. push.delivered)
X-Sendexa-DeliveryUnique delivery attempt id
User-AgentSendexa-Webhooks/1.0

Verify HMAC (Node.js)

JavaScript
import crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: use raw body for signature verification
app.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 processed
switch (event.type) {
case 'push.delivered':
// mark message delivered in DB
break;
case 'push.failed':
if (['UNREGISTERED', 'INVALID_TOKEN'].includes(event.data?.error?.code)) {
// DELETE /v1/push/devices/:token
}
break;
case 'push.opened':
// analytics / attribution
break;
}
res.status(200).json({ received: true });
}
);

Verify HMAC (Python)

Python
import hmac
import hashlib
from flask import Flask, request, abort
app = 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
Retry policy

Your endpoint should respond with 2xx within 10 seconds. Any other status (or timeout) schedules a retry:

AttemptDelay after previous
1Immediate
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.id for idempotency — retries may deliver the same event more than once.
  • Delivery attempt id is in X-Sendexa-Delivery.
Fan-out behaviour

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.

Operational checklist
  • HTTPS only; valid public certificate (no self-signed in production).
  • Return 2xx only after signature verification and durable enqueue/store.
  • On push.failed with non-retryable token errors, call DELETE /v1/push/devices/:token.
  • Store pushId when sending so support can trace a user complaint to webhook history.
  • Monitor webhook success rate in Dashboard → Webhooks → Logs.