Orders & Postbacks  ›  Signing & Security

Signing & Security

Every postback PriceFirst sends is signed with a shared secret token and an HMAC-SHA256 signature over the raw request body. Verifying both is required to trust incoming webhook traffic.

Headers

HeaderDescription
X-PriceFirst-TokenThe postbackToken from your marchant profile. Opaque shared secret.
X-PriceFirst-TimestampUnix epoch seconds at the moment the signature was generated.
X-PriceFirst-SignatureHex-encoded HMAC-SHA256 of "<timestamp>.<rawBody>" using postbackHmacSecret.
X-PriceFirst-AlgorithmAlways HMAC-SHA256 for this version.
X-PriceFirst-IdempotencyThe order's order_code (same value as in the body).

Verification algorithm

  1. Read the raw request body as a UTF-8 string before parsing the JSON. Do not re-serialize — whitespace and key order must match what PriceFirst signed.
  2. Compare X-PriceFirst-Token against your stored postbackToken. Reject if it doesn't match.
  3. Reject if |now - X-PriceFirst-Timestamp| > 300 (5-minute clock-skew window).
  4. Compute expected = HMAC-SHA256(postbackHmacSecret, timestamp + "." + rawBody) (hex).
  5. Compare expected against X-PriceFirst-Signature using a constant-time comparison (e.g. crypto.timingSafeEqual in Node).
  6. Only after all three pass, parse the JSON and process the order.

Always verify before parsing. If you parse and re-stringify the body before comparing, the HMAC will never match — JSON whitespace and key order are not canonicalised.

Reference implementations

Pick your stack. Every snippet performs the same three checks in the order the algorithm above prescribes — token match, timestamp window, HMAC-SHA256 equality — using the language's built-in constant-time comparator.

const crypto = require('node:crypto');

function verifyPostback({ rawBody, headers, expectedToken, hmacSecret }) {
const token = headers['x-pricefirst-token'];
const timestamp = headers['x-pricefirst-timestamp'];
const supplied = headers['x-pricefirst-signature'];

if (token !== expectedToken) return false;

const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) return false;

const expected = crypto
  .createHmac('sha256', hmacSecret)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');

const a = Buffer.from(expected, 'hex');
const b = Buffer.from(supplied || '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Transport

  • postBackUrl must be HTTPS. Plain HTTP postbacks are not sent.

Data sensitivity

The payload contains PII and, for BACS orders, bank sort code + account number. Treat it accordingly:

  • TLS end-to-end.
  • Never log the raw body or attempt details.
  • Avoid forwarding to third-party logging tools without masking.