Authentication

All PayNearMe API calls and callbacks must include a hash-based message authentication code (HMAC) signature. An HMAC signature is a string of characters that authenticate messages received from the API. This authentication method protects the integrity of request messages and helps to prevent malicious attacks like cross-site scripting and brute-force attacks.

Calculating the Signature

HMAC signature values are calculate by running your API Secret Key and the alphabetized, concatenated parameters of the request's payload through the SHA256 message digest algorithm.

For example, let's assume you want to create an order by sending the following request parameters to the /create_order endpoint:

curl --request POST \
     --url https://api.paynearme-sandbox.com/json-api/create_order \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "site_identifier": "S2155373459",
  "timestamp": "1636142061",
  "version": "3.0",
  "signature": "",
  "order_amount": "500",
  "order_currency": "USD",
  "site_customer_identifier": "11223344",
  "order_type": "any",
  "order_is_standing": "true"
}
'

To calculate the signature value, you would first sort the parameters alphabetically by name and then concatenate them into a single string of characters:

order_amount500order_currencyUSDorder_is_standingtrueorder_typeanysite_customer_identifier11223344site_identifierS2155373459timestamp1636142061version3.0

Using your secret key of ab7b539ea1317cca67c63c552 in the SHA256 digest, the computed value of the signature would be the following:

65c694f4b632187aa02fc4144cdd374373307f0a200448c9e53f2e47d84b3b82

The full call that you would submit to PayNearMe would include this signature value:

curl --request POST \
     --url https://api.paynearme-sandbox.com/json-api/create_order \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "site_identifier": "S2155373459",
  "timestamp": "1636142061",
  "version": "3.0",
  "signature": "65c694f4b632187aa02fc4144cdd374373307f0a200448c9e53f2e47d84b3b82",
  "order_amount": "500",
  "order_currency": "USD",
  "site_customer_identifier": "11223344",
  "order_type": "any",
  "order_is_standing": "true"
}
'

Use the following code samples to automatically generate your signatures while developing your application:

#!/usr/bin/env ruby

require 'openssl'

#
# Generate a PayNearMe API V3 signature in plain Ruby
#

def pnm_signature(secret_key, params)

  # Do not use any symbolized keys, only strings
  plain_params = params.transform_keys(&:to_s)
  
  # These parameters must always be present
  required_params = %w(version site_identifier timestamp)

  # These parameters should never be included in the signature
  exempt_params = %w(format signature call)
  
  # Throw an error if a required param is missing
  required_params.each do |a|
    raise "Missing required parameter: #{a}" unless plain_params.has_key?(a)
  end

  # Remove any exempt params
  # Sort alphabetically by key
  # Combine into a single string without any extra characters (ex, no '&')
  string_to_sign = plain_params.reject{ |key,v|
    exempt_params.include? key
  }.sort.flatten.join

  # Return the signature of the secret_key + string_to_sign 
  OpenSSL::HMAC.hexdigest('sha256', secret_key, string_to_sign).downcase
end

secret = 'abc123' # store somewhere safe - do not hard code
params = {
  'version': '3.0',
  'site_identifier': 'MySiteID',
  'timestamp': Time.now.to_i.to_s, # generate a unix timestamp. Ex: '1582149833'
  'hello': 'world', 
  'foo': 'bar',
  'format': 'json'
}
puts "Signature: " + pnm_signature(secret, params)

# example:
#  With the above params and a timestap of 1582149833
#  you should get a signature of
#  "7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc"
<?php 

//
// Generate a PayNearMe API V3 signature in PHP
//
// example:
//   $timestamp = explode(" ",microtime())[1]; # ex: 1582149833
//   echo pnm_signature( 'abc123',
//     ["hello"=>'world', "foo"=>'bar', "version"=>'3.0', "site_identifier"=>'MySiteID',"timestamp"=>$timestamp]
// )
// output:
//   7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc
//
function pnm_signature($secret_key, $params){
  
  // These parameters must always be present
  $required_params = ["version","site_identifier","timestamp"];

  // These parameters should never be included in the signature
  $exempt_params = ["format","signature", "call", "secret_key"];
    
  // Throw an error if a required param is missing
  $missing_params = array_diff($required_params, array_keys($params));
  if( count($missing_params) > 0 ){
    throw new Exception('Missing required parameter(s): '.join($missing_params,', '));
  }

  // Sort alphabetically by key
  ksort($params);

  // Combine into a single string without any extra characters (ex, no '&')
  // skipping any exempt params
  $string_to_sign = "";
  foreach($params as $k => $v){ 
    if( in_array($k, $exempt_params)) next;
    $string_to_sign .= $k . $v; 
  }

  // Return the signature of the secret_key + string_to_sign 
  return hash_hmac('sha256', $string_to_sign, $secret_key); 
}


?>
/*
 * Generate a PayNearMe API V3 Signature with Node.js
 *
 * example from command line:
 * 
 *   node paynearme_api_signature_example.js abc123 '{"foo":"bar","hello":"world","site_identifier":"MySiteID","version":"3.0","timestamp":1582149833}'
 *    => 7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc
 */
const crypto = require('crypto');
function pnm_signature(secret_key, params){
  // These parameters must always be present
  var required_params = ["version", "site_identifier", "timestamp"];

  // These parameters should never be included in the signature
  var exempt_params = ["format", "signature", "call"];
  
  // Convert params to object if passed in as a string
  if( typeof(params) == 'string' ){
    params = JSON.parse(params);
  }

  // Sort alphabetically by key
  var param_keys = Object.keys(params).sort();

  // Throw an error if a required param is missing
  required_params.forEach( function(e){ 
    if( param_keys.indexOf(e) == -1 )
      throw( "Missing required parameter: " + e); 
  }); 

  // Combine into a single string without any extra characters (ex, no '&')
  // skipping any exempt params
  var string_to_sign = "";
  param_keys.forEach( function(k){ 
    if( exempt_params.indexOf(k) != -1 ){ return; }
    string_to_sign += k + params[k]; 
  });

  // Return the signature of the secret_key + string_to_sign 
  return crypto.createHmac('sha256', secret_key).update(string_to_sign).digest('hex');
}


var myArgs = process.argv.slice(2);
console.log( " => " + pnm_signature(myArgs[0], myArgs[1]));
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Collections.Specialized;


namespace SignParams {

  public struct PnmSignature {

    public static string Sign(NameValueCollection apiParams, string secretKey) {
      
      // Note that "version", "site_identifier", and "timestamp" are always required.
      // but "format","signature" and "call" should never be included in the signature.
      string[] exempt_params = { "format", "signature", "call" };

      string[] qKeys = apiParams.AllKeys;
      Array.Sort(qKeys);

      String sortedParams = "";
      foreach (var item in qKeys){
        if( !Array.Exists(exempt_params, element => element == item) ){
          sortedParams += item + apiParams[item];
        }
      }
      
      ASCIIEncoding encoding = new ASCIIEncoding();
      byte[] secretKeyBytes = encoding.GetBytes(secretKey);
      byte[] paramBytes = encoding.GetBytes(sortedParams);

      // compute the hash
      HMACSHA256 algorithm = new HMACSHA256(secretKeyBytes);
      byte[] hashBytes = algorithm.ComputeHash(paramBytes);

      // convert the bytes to a hex string
      string signature = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
      return signature;
    }
  }

  class Program {

    static void Main() {
    
      // Note: You should store your private key someplace safe and read it into your code
      const string secretKey = "abc123"; // replace with your secret key
      const string apiVersion = "3.0";
      const string siteIdentifier = "MySiteID";
      // Need to pass time since epoc as an integer
      DateTimeOffset timestamp = DateTimeOffset.Now;

      // Starting in API Version 3.0, all parameters must be passed via POST
      // and CANNOT be in the query string of the URL.
      NameValueCollection apiParams = new NameValueCollection();
      apiParams.Add("version", apiVersion);
      apiParams.Add("site_identifier", siteIdentifier);
      apiParams.Add("timestamp", timestamp.ToUnixTimeSeconds().ToString());
      apiParams.Add("foo", "bar");
      apiParams.Add("hello", "world"); 
      apiParams.Add("format", "json");      
      
      // Ex: with a timestamp of '1582149833' and the rest of the above params,
      // you should get a signature of 
      // 7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc
      Console.WriteLine(PnmSignature.Sign(apiParams, secretKey));
    }
  }
}
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
import java.util.Arrays;
import java.util.TreeMap;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.binary.Base64;


public class PnmSignature {

  // Note: You should store your private key someplace safe and read them into your code
  private static String secretKey = "abc123"; // replace with your secret key
  private static String apiVersion = "3.0";
  private static String siteIdentifier = "MySiteID"; // replace with your unique Merchant ID

  // This variable stores the binary key, which is computed from the string (Base64) key
  private static byte[] key;
  private String sortedParams;
  
  public static void main(String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException, URISyntaxException {
    // Starting in API Version 3.0, all parameters must be passed in via POST
    // and CANNOT be in the query string of the URL
    Long timestamp = Instant.now().getEpochSecond();
    Map<String, String> apiParams = new HashMap<>();
    apiParams.put("version", apiVersion);
    apiParams.put("site_identifier", siteIdentifier);
    apiParams.put("timestamp", Long.toString(timestamp)); // EX: "1582149833"
    apiParams.put("foo", "bar");
    apiParams.put("hello", "world"); 
    apiParams.put("format", "json");

    PnmSignature signer = new PnmSignature(); 
    String signature = signer.sign(apiParams, secretKey);
    
    // Ex: with a timestamp of '1582149833' and the rest of the above params,
    // you should get a signature of 
    // 7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc
    System.out.println("Signature: " + signature);
  }
  

  public String sign(Map apiParams, String secretKey) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, URISyntaxException {
    // Note that "version", "site_identifier", and "timestamp" are always required.
    // but these should never be included in the signature.
    List<String> exempt_params = Arrays.asList("format", "signature", "call");

    // sort by key then combine into a single string
    Map<String, String> treeParams = new TreeMap<String, String>(apiParams);
    this.sortedParams = "";
    treeParams.forEach((key, value) -> {
      if( !exempt_params.contains(key) ){
        this.sortedParams += key + value;
      }
    });

    // Compute the HMAC SHA256 signature in Hex format using the secret key and the computed string
    Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
    SecretKeySpec secret_key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
    sha256_HMAC.init(secret_key);
    String signature = Hex.encodeHexString(sha256_HMAC.doFinal(this.sortedParams.getBytes()));

    return signature;
  }
}
#!/usr/bin/python
# -*- coding: utf-8 -*-
""" Generate a PayNearMe API V3 signature """

import base64
import hashlib
import hmac
import time

def pnm_signature(secretKey, params):
  """ Generate a PayNearMe API V3 signature given a secret key and a set of params.

      Usage:
      from pnm_signature secretKey params

      signature = pnm_signature(secretKey, params)

      Args:
      secretKey - Your API Secret Key
      params - A hash/dictionary of parameters that will be passed to the API

      Returns:
      The hex formatted signature string
  """

  if not params or not secretKey:
    raise Exception("Both secretKey and params are required")

  # These parameters should never be included in the signature
  exempt_params = ['format', 'signature', 'call']

  # Turn the params into a sorted string
  string_to_sign = '';
  for key in sorted (params.keys()):
    if key not in exempt_params:
      string_to_sign += key + params[key];

  # Return the hex signature of the secret_key + string_to_sign
  raw_signature = hmac.new(secretKey, string_to_sign, hashlib.sha256)
  return raw_signature.hexdigest()


if __name__ == "__main__":
  params = {
    'version': '3.0',
    'site_identifier': 'MySiteID',
    'timestamp': str(int(time.time())), # generate a unix timestamp. Ex: '1582149833'
    'hello': 'world', 
    'foo': 'bar',
    'format': 'json'
  }
  secret = 'abc123' # store somewhere safe - do not hard code
  print "Signature: " + pnm_signature(secret, params)
  # With the above parameters and a timestamp of 1582149833
  # you should get a signature of 
  # 7986b4e59c15cd22fd496113c916f9739f619778812bf9ab8943af80749aadcc

Troubleshooting Signature Errors

Calculating API signatures can be challenging, particularly for complex requests involving multiple data parameters. The checklist below offers a structured approach to diagnosing and resolving common signature-related errors during API testing.

☑️ Are You Using the Correct Site ID?

The Site ID is the identifier that maps to a unique site configuration. You can find your Site ID in the PayNearMe Business Portal under Developer > API Documentation > API Keys and Signatures.

When managing API keys for multiple child sites, always ensure that each secret key corresponds to the correct Site ID.

☑️ Check Your Decimal Values

Parameters that use the DECIMAL data type should include two digits after the decimal point. Decimal parameters include amount fields (e.g., order_amount, minimum_payment_amount, auto_pay_amount, etc.).

☑️ Verify the Parameters are Sorted Alphabetically

Time to brush up on your A-B-Cs to ensure the parameters are correctly sorted. When concatenating, the name of the parameter goes first (e.g., timestamp1636142061 NOT 1636142061timestamp). Then, use the first letter of the parameter name when sorting.

☑️ Check for Special Characters or Spaces in the String Parameters

Special characters do not play nice with PayNearMe's API. In fact, the only special characters allowed in signature calculations include the following:

  • Hyphen -
  • Underscore _
  • Period .

If a random question mark or any other special character gets thrown in the mix, the call will error out. Similarly, ensure you've not inadvertently add spaces within the parameter values. For fields containing proper names, use either JohnDoe or John_Doe, but definitely not John Doe.

☑️ Verify the Default Key Version Number

Your default API key is the key pair used for verifying callbacks. The version number of your default key must match the API version you're using to structure and make calls. For example, a version 2.0 default secret key cannot be used in the signature of a version 3.0 API call.

☑️ Verify the Key Pair

When using multiple API keys for a site, include the Key Identifier (prefixed with K) in the site_identifier parameter of your request. The Key Identifier is a unique value tied to each key pair and is backward-compatible with the Site Identifier (prefixed with S). Providing the Key Identifier (instead of the Site Identifier) allows PayNearMe's API to accurately resolve and apply the corresponding Secret Key during request processing.

curl --request POST \
     --url https://api.paynearme-sandbox.com/json-api/create_order \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "site_identifier": "K6861123267",
  "timestamp": "1636142061",
  "version": "3.0",
  "signature": "28d9e2c7869fec651dc9b59bea4eff3263c079a330f97712bf03c2a5b2c76aea",
  "order_amount": "500",
  "order_currency": "USD",
  "site_customer_identifier": "11223344",
  "order_type": "any",
  "order_is_standing": "true"
}
'

If you choose to use the Site Identifier when making requests, you must use the secret key associated with your default key pair; otherwise, the request will fail.

☑️ Check Your Booleans

PayNearMe's API loves string data types, so always ensure your Boolean parameters are sent as strings in your request.