Webhooks
Pushtifly events on your server, with a verifiable signature and retries.
Set up an https URL in the panel's Webhooks, choose the events and, if you want, restrict it to one app. Pushtifly will send you a POST with JSON every time one happens.
Events
| Event | When |
|---|---|
notification.completed |
A send finished (with its statistics) |
notification.failed |
A send failed completely |
delivery.failed |
Failed deliveries of a send (grouped per batch) |
device.invalidated |
Tokens that Google or Apple invalidated (uninstalls), grouped |
credential.expiring / credential.expired / credential.invalid |
A credential is about to expire, expired or was rejected |
quota.warning / quota.exceeded |
You reached 80% or 100% of the monthly quota |
Headers
X-Pushtifly-Event: notification.completed
X-Pushtifly-Delivery: 01JD… (unique per delivery attempt)
X-Pushtifly-Timestamp: 1798012345
X-Pushtifly-Signature: sha256=5f2c…
X-Pushtifly-Attempt: 1
Verify the signature
The signature is HMAC-SHA256(secret, timestamp + "." + body). The secret (whsec_…) is shown when you create the webhook or rotate it. Reject timestamps older than 5 minutes (anti-replay) and compare in constant time:
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PUSHTIFLY_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('PUSHTIFLY_WEBHOOK_SECRET'));
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, $_SERVER['HTTP_X_PUSHTIFLY_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
import crypto from 'node:crypto';
// Use the RAW (unparsed) body: express.raw({ type: 'application/json' })
function verify(req, secret) {
const ts = req.get('X-Pushtifly-Timestamp');
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${req.body}`).digest('hex');
const given = req.get('X-Pushtifly-Signature') ?? '';
return Math.abs(Date.now() / 1000 - Number(ts)) <= 300
&& given.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
import hmac, hashlib, time
def verify(body: bytes, ts: str, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - int(ts)) <= 300 and hmac.compare_digest(expected, signature)
Response and retries
- Respond 2xx in under 10 seconds. Process in the background if it takes longer.
- If you fail, it's retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h.
- After 50 failures in a row, the webhook is disabled and we notify you.
- Use
X-Pushtifly-Deliveryor the event'sidto ignore duplicates.
In the panel you see each delivery, with its response, and you can resend or test the webhook.
Security restrictions
Only public https URLs, on ports 443 or 8443, and no redirects. Private or internal IPs are rejected.