Skip to content

Node.js

import crypto from 'node:crypto';

// 1. Build the body ONCE (a string, not an object)
const payload = JSON.stringify(requestData);

// 2. Sign that exact string -> lowercase hex
const signature = crypto
  .createHmac('sha256', signKey)
  .update(payload, 'utf8')
  .digest('hex');

// 3. Send the SAME string you signed
const response = await fetch('https://api.napoleonpay.com/api/v1/payments', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${apiKey}`,
    Signature: signature,
  },
  body: payload,
});

Node.js

JSON.stringify already drops the .0 from whole numbers, but it emits raw UTF-8 where PHP escapes it, and keeps {} for empty objects — both are corrected below.

import crypto from 'node:crypto';

const canonicalise = (v) =>
  Array.isArray(v)
    ? v.map(canonicalise)
    : v && typeof v === 'object'
      ? (Object.keys(v).length
          ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, canonicalise(x)]))
          : [])                                    // empty object -> []
      : v;

const escapeNonAscii = (s) =>
  s.replace(/[€-￿]/g, (c) =>
    '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));

// rawBody must be the untouched request body string, not a re-serialized object
export function verifyCallback(rawBody, signatureHeader, signKey) {
  const canonical = escapeNonAscii(
    JSON.stringify(canonicalise(JSON.parse(rawBody)), null, 4)
  );
  const expected = crypto
    .createHmac('sha256', signKey)
    .update(canonical, 'utf8')
    .digest('hex');

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

In Express, register the route with express.raw({ type: 'application/json' }) so you receive the original bytes — express.json() parses and discards them.