APXRAILSdocs

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

node — @apxrails/sdk-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
<?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.succeeded
  • payment_intent.failed
  • checkout.session.completedcarries payment_link when the session came from one
  • refund.succeeded
  • refund.failed
  • dispute.createdcarries evidence_due_by
  • dispute.closed