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
| Header | Description |
|---|---|
X-PriceFirst-Token | The postbackToken from your marchant profile. Opaque shared secret. |
X-PriceFirst-Timestamp | Unix epoch seconds at the moment the signature was generated. |
X-PriceFirst-Signature | Hex-encoded HMAC-SHA256 of "<timestamp>.<rawBody>" using postbackHmacSecret. |
X-PriceFirst-Algorithm | Always HMAC-SHA256 for this version. |
X-PriceFirst-Idempotency | The order's order_code (same value as in the body). |
Verification algorithm
- 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.
- Compare
X-PriceFirst-Tokenagainst your storedpostbackToken. Reject if it doesn't match. - Reject if
|now - X-PriceFirst-Timestamp| > 300(5-minute clock-skew window). - Compute
expected = HMAC-SHA256(postbackHmacSecret, timestamp + "." + rawBody)(hex). - Compare
expectedagainstX-PriceFirst-Signatureusing a constant-time comparison (e.g.crypto.timingSafeEqualin Node). - 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
postBackUrlmust 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.