PerxDevelopers

Core concepts

Authentication

Every request carries four headers and an HMAC signature over the raw body. This is the part integrations get wrong most often, so it is worth reading once, carefully.

Connections and secrets

Every merchant × store pair is one connection in Perx, and every connection has its own secret — a 64-character hex string. The merchant creates it in their dashboard, copies it, and pastes it into your product alongside their store ID.

One connection per store, not per merchant

A merchant with three outlets creates three connections, so Perx can attribute each sale to the right outlet for branch-scoped rewards. Your settings UI must therefore be per store. If you build a single global settings screen, multi-outlet merchants cannot use your integration correctly.

  • Secrets are rotatable from the Perx dashboard. Your settings screen must accept a new secret without redoing setup.
  • Treat the secret as a credential: encrypt at rest, never log it, never expose it to a browser.

The four headers

HeaderValue
X-Perx-VendorYour vendor slug, assigned by Perx at onboarding, e.g. acmepos. Lowercase and permanent.
X-Perx-StoreThe store identifier on your side, exactly as the merchant entered it in Perx. This is what routes the request to the right merchant and outlet.
X-Perx-TimestampUnix epoch seconds at the moment of signing, as a decimal string. Not milliseconds.
X-Perx-Signaturev1= followed by the base64 signature below.

Computing the signature

The signature covers the timestamp and the raw body, joined by a single ASCII period. Binding the timestamp into the signature is what makes a captured request un-replayable.

text
signingString = X-Perx-Timestamp + "." + rawRequestBody
signature     = "v1=" + base64( HMAC_SHA256( signingString, connectionSecret ) )
Sign the bytes you actually transmit

Serialising to JSON, signing the result, then re-serialising to send produces a different byte sequence — different key order, different whitespace, different unicode escaping — and the signature will fail. Serialise once into a buffer or string, sign that, send that. This single mistake accounts for most first-week 401s.

Reference implementations

const crypto = require('crypto');

function signedRequest(payload, vendorSlug, storeId, secret) {
  // Serialise ONCE. Sign this buffer, send this buffer.
  const raw = Buffer.from(JSON.stringify(payload), 'utf8');
  const ts  = Math.floor(Date.now() / 1000).toString();

  const sig = crypto.createHmac('sha256', secret)
                    .update(ts + '.')
                    .update(raw)
                    .digest('base64');

  return {
    headers: {
      'Content-Type':     'application/json',
      'X-Perx-Vendor':    vendorSlug,
      'X-Perx-Store':     storeId,
      'X-Perx-Timestamp': ts,
      'X-Perx-Signature': 'v1=' + sig,
    },
    body: raw,
  };
}

The replay window

Perx rejects any request whose X-Perx-Timestamp is more than 300 seconds from server time, in either direction. Keep your servers on NTP.

This has a consequence for queued deliveries: if a webhook sits in your retry queue for an hour, you must re-sign it with a fresh timestamp before sending, not replay the original headers. The event’s own occurredAt stays as it was — backdated events are accepted and earn correctly.

Transport and data handling

  • TLS 1.2 or higher. Perx does not accept plaintext HTTP.
  • Send only the customer identifier you already hold. Do not send addresses, notes, or any payment instrument data — there is no field for it and unknown properties are rejected.
  • The payload limit is 1 MB, enforced at the gateway. A larger body is truncated and then fails parsing as a 400.

Debugging a 401

In order of likelihood: you re-serialised the body after signing; you used milliseconds instead of seconds; your clock has drifted; you signed the body without the timestamp + "." prefix; you omitted the v1= prefix; the merchant rotated their secret.

The signature playground takes your secret, body and timestamp and shows the exact expected value, so you can diff it against what your code produced.