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-Delivery or the event's id to 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.