Webhooks
APXRAILS signs every webhook with an HMAC so you can prove it came from us. Verify the signature, ACK fast, and re-query the API before acting — the webhook is a nudge, the API is the truth.
The signature
Each delivery carries an apx-signature header:
apx-signature: t=1783539464,v1=5f8a1c…
v1 is HMAC-SHA256 over `${t}.${rawBody}`keyed with your endpoint's signing secret (whsec_…, shown once at endpoint creation). During the 30-day secret-rotation grace window the header carries two v1 entries — any match passes. Reject timestamps older than 5 minutes (replay window) and always verify the raw request bytes, never a re-serialized parse.
Node
import { constructEvent } from '@apxrails/sdk-node';
app.post('/webhooks/apxrails', express.raw({ type: '*/*' }), (req, res) => {
let event;
try {
event = constructEvent({
payload: req.body,
header: req.headers['apx-signature'],
secret: process.env.APX_WEBHOOK_SECRET,
});
} catch {
return res.status(401).end();
}
switch (event.type) {
case 'checkout.session.completed': /* re-query, then fulfill */ break;
case 'refund.succeeded': /* update your order state */ break;
case 'dispute.created': /* alert someone — deadlines are real */ break;
}
res.status(200).end();
});PHP
<?php
function apx_verify_webhook(string $payload, ?string $header, string $secret,
int $tolerance = 300): bool {
if ($header === null) return false;
$timestamp = null; $signatures = [];
foreach (explode(',', $header) as $kv) {
[$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
if (trim($k) === 't') $timestamp = (int) trim($v);
if (trim($k) === 'v1') $signatures[] = trim($v);
}
if (!$timestamp || !$signatures) return false;
if (abs(time() - $timestamp) > $tolerance) return false;
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) return true;
}
return false;
}
$payload = file_get_contents('php://input'); // RAW body
$header = $_SERVER['HTTP_APX_SIGNATURE'] ?? null;
if (!apx_verify_webhook($payload, $header, getenv('APX_WEBHOOK_SECRET'))) {
http_response_code(401); exit;
}
$event = json_decode($payload, true);
http_response_code(200);Delivery semantics
Deliveries retry with exponential backoff for ~3 days; an endpoint failing ~50 consecutive deliveries is auto-disabled (you'll see it in the dashboard). Deliveries can arrive out of order and more than once — key your processing on event.id and re-query the resource for current state. Every delivery, request and response body included, is inspectable and replayable in Developers → Webhooks.
Event types
payment_intent.succeededpayment_intent.failedcheckout.session.completedcarries payment_link when the session came from onerefund.succeededrefund.faileddispute.createdcarries evidence_due_bydispute.closed