Validate signature of requests

MoonPay will sign all requests sent to our partners that include the following.

  • signature - HEX hmac request signature
  • timestamp - UNIX time in seconds of when the signature has been generated.

The parameters mentioned above will be present in the request headers:

  • X-SIGNATURE-V2
  • X-TIMESTAMP

How to validate the signature?

Consider the following example:

https://api.moonpay.com/v3/nft/asset_info/0x2953399124f0cbb46d2cbacd8a89cf0599974963/1?listingId=19
X-SIGNATURE-V2: ecb073e45654b47f3edf0c805e3d7d4f3069d3d63ca4e83b8c3f97b2cd914009
X-TIMESTAMP:1645556506
  1. Check if the timestamp is valid (within 30s from now).
  2. Generate signature using your secret key. To do that you will need:
    • secret key: sk_test_key
    • endpoint URI, note that the base URLis excluded, but all query parameters should be included: /asset_info/0x2953399124f0cbb46d2cbacd8a89cf0599974963/1?listingId=19
    • HTTP request method in upper case: GET
    • timestamp: 1645556506
    • stringified bodyif the request method is POST, PUT or PATCH.
      We use hmac with sha256 digested to hex to generate a signature.
  3. Compare signature from the request with the one you just generated (remember to use safe method of comparison likecrypto.timingSafeEqual in nodejs or hmac.compare_digest in Python).

📘

Query parameters in endpoint requests

When making requests to your partner endpoints, MoonPay will send your query parameters in alphabetical order.

Be aware that we occasionally add new query parameters to the requests we make to your endpoints, and that certain cloud providers and their API gateway may change the order of our parameters resulting in a failed signature validation. When implementing these endpoints you should account for these query parameters, as using a pre-defined set of query parameters may result in signature validation issues.

Example with TypeScript/NodeJS

import crypto from 'crypto';

const SIGNATURE_VALID_FOR = 30;

function getTimestamp(): number {
  return Math.round(new Date().getTime() / 1000);
}

function validateTimestamp(timestamp: number): boolean {
  return getTimestamp() - timestamp <= SIGNATURE_VALID_FOR;
}

function generateSignature(
  secretKey: string,  // your secret key e.g. sk_test_6CDFl9eVtnRpiJsKu6tHD3jye9AW2u
  uri: string,        // do not include your base URL
  method: string,     // HTTP request method in upper case 
  timestamp: number,  // timestamp is a number
  body?: string,      // do not include for GET requests, make sure it's stringified
): string {
                      // order has to be the same
  let payload = `${method};${uri};${timestamp}`;
  if (body) {
    payload += `;${body}`;
  }
  return crypto
    .createHmac('sha256', secretKey)
    .update(payload)
    .digest('hex');
}

function verifySignature(
  signature: string,
  secretKey: string, 
  uri: string,       
  method: string,  
  timestamp: number, 
  body?: string,      
): boolean {
  const generatedSignature = generateSignature(
    secretKey, 
    uri, 
    method, 
    timestamp, 
    body
  );

  if (
    !crypto.timingSafeEqual(
      Buffer.from(generatedSignature, 'hex'),
      Buffer.from(signature, 'hex'),
    )
  ) {
    return false;
  }

  if (!validateTimestamp(timestamp)) {
    return false;
  }

  return true;
}