These are the Pay1st best practices for generating signatures for gateway Integrations
Overview
Requests that create a payment (Create Payment Request) or a refund (Refund Request) must be signed. Generate a signature for the request body and send it in the X-Signature HTTP header, together with the timestamp used in the X-Timestamp HTTP header. We do this so that we can validate that these requests have not been tampered with.
Signature Description
The signature is a lowercase, hex encoded HMAC-SHA256 and is sent in the X-Signature HTTP request header.
The signature can be generated using the following steps:
- Generate a timestamp — the current time as an ISO-8601 UTC date-time ending in
Z, for example2025-03-17T08:10:52.544Z. A value without a time zone (for example2025-03-17T08:10:52) or in epoch milliseconds is not accepted. - Build the string to sign — the timestamp immediately followed by the request body:
timestamp + body. Add nothing in between (no space, no newline, no separator).- The body must be exactly the bytes you send in the HTTP request. Do not trim, pretty-print or re-serialise it after signing. Even one extra space or a trailing newline will change the signature.
- Compute the HMAC-SHA256
- Take your Signing Key (obtained from Configuring API Credentials) exactly as issued. Do not base64-decode it.
- Convert the Signing Key to bytes using UTF-8. These bytes are the HMAC key.
- Convert the string from step 2 to bytes using UTF-8. These bytes are the data (the message).
- Calculate
HMAC-SHA256(key, data)with the HMAC key from 3.2 and the data from 3.3. The result is 32 bytes.
- Hex encode the result in lowercase — hex encode the 32 bytes from step 3. This gives a 64-character string. Some languages return uppercase by default (for example C#
ToString("X2")), so convert it to lowercase. - Send the headers — add both headers to the request:
X-Signature: the 64-character lowercase hex string from step 4.X-Timestamp: the timestamp from step 1, exactly the same string that was used in step 2.
TimestampThe
X-Timestampheader must contain exactly the same timestamp string that was used to build the signature. The value is compared character for character, so do not reformat it between signing and sending.The timestamp must also be within 5 minutes of the current server time (either earlier or later). Requests outside this window are rejected even when the signature is correct, so make sure your server clock is synchronised (for example with NTP).
Test Cases
Use the values below to check your implementation. Signing the Timestamp followed by the Request Body with the Signing Key must produce the Expected Signature.
Signing Key
hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8YTimestamp (X-Timestamp)
2025-03-17T08:10:52.544247646ZRequest Body (a single line, with no trailing newline)
{"countryCode":"NG","currencyCode":"NGN","amount":100,"partnerReference":"4a662291-2840-4bcd-aae9-553ab24601ed","callbackSuccessUrl":"https://callback.success","callbackFailureUrl":"https://callback.failure","callbackPendingUrl":"https://callback.pending","callbackCancelUrl":"https://callback.cancel","webhookUrl":"https://webhook.success","products":[{"title":"10 Credits","sku":"12345","price":100,"quantity":1,"currencyCode":"NGN"}],"metadata":[{"key":"customerId","value":"1"}]}Expected Signature (X-Signature)
80cd1f08a85f0eee8bcf067fb5be0e78ea499d65d8c40b5abf91dc74f3a60bd8Code Samples
The following code samples can be used as a reference (or the signature description can be code generated using an LLM). Each sample uses the values from the test case above and prints the expected signature 80cd1f08a85f0eee8bcf067fb5be0e78ea499d65d8c40b5abf91dc74f3a60bd8. In a real integration, replace the hard-coded timestamp with the current UTC time and sign the exact body you send.
SIGNING_KEY='hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y'
TIMESTAMP='2025-03-17T08:10:52.544247646Z' # real request: date -u +%Y-%m-%dT%H:%M:%SZ
BODY='{"countryCode":"NG","currencyCode":"NGN","amount":100,"partnerReference":"4a662291-2840-4bcd-aae9-553ab24601ed","callbackSuccessUrl":"https://callback.success","callbackFailureUrl":"https://callback.failure","callbackPendingUrl":"https://callback.pending","callbackCancelUrl":"https://callback.cancel","webhookUrl":"https://webhook.success","products":[{"title":"10 Credits","sku":"12345","price":100,"quantity":1,"currencyCode":"NGN"}],"metadata":[{"key":"customerId","value":"1"}]}'
printf '%s%s' "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$SIGNING_KEY" \
| awk '{print $NF}'<?php
$signingKey = 'hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y';
$timestamp = '2025-03-17T08:10:52.544247646Z'; // real request: gmdate('Y-m-d\TH:i:s\Z')
$body = '{"countryCode":"NG","currencyCode":"NGN","amount":100,"partnerReference":"4a662291-2840-4bcd-aae9-553ab24601ed","callbackSuccessUrl":"https://callback.success","callbackFailureUrl":"https://callback.failure","callbackPendingUrl":"https://callback.pending","callbackCancelUrl":"https://callback.cancel","webhookUrl":"https://webhook.success","products":[{"title":"10 Credits","sku":"12345","price":100,"quantity":1,"currencyCode":"NGN"}],"metadata":[{"key":"customerId","value":"1"}]}';
$signature = hash_hmac('sha256', $timestamp . $body, $signingKey); // lowercase hex
echo $signature . PHP_EOL;import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class Signature {
public static void main(String[] args) throws Exception {
String signingKey = "hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y";
String timestamp = "2025-03-17T08:10:52.544247646Z"; // real request: java.time.Instant.now().toString()
String body = "{\"countryCode\":\"NG\",\"currencyCode\":\"NGN\",\"amount\":100,\"partnerReference\":\"4a662291-2840-4bcd-aae9-553ab24601ed\",\"callbackSuccessUrl\":\"https://callback.success\",\"callbackFailureUrl\":\"https://callback.failure\",\"callbackPendingUrl\":\"https://callback.pending\",\"callbackCancelUrl\":\"https://callback.cancel\",\"webhookUrl\":\"https://webhook.success\",\"products\":[{\"title\":\"10 Credits\",\"sku\":\"12345\",\"price\":100,\"quantity\":1,\"currencyCode\":\"NGN\"}],\"metadata\":[{\"key\":\"customerId\",\"value\":\"1\"}]}";
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = hmac.doFinal((timestamp + body).getBytes(StandardCharsets.UTF_8));
String signature = HexFormat.of().formatHex(hash); // lowercase hex (Java 17+)
System.out.println(signature);
}
}using System;
using System.Security.Cryptography;
using System.Text;
class Signature
{
public static void Main(string[] args)
{
string signingKey = "hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y";
string timestamp = "2025-03-17T08:10:52.544247646Z"; // real request: DateTime.UtcNow.ToString("o")
string body = "{\"countryCode\":\"NG\",\"currencyCode\":\"NGN\",\"amount\":100,\"partnerReference\":\"4a662291-2840-4bcd-aae9-553ab24601ed\",\"callbackSuccessUrl\":\"https://callback.success\",\"callbackFailureUrl\":\"https://callback.failure\",\"callbackPendingUrl\":\"https://callback.pending\",\"callbackCancelUrl\":\"https://callback.cancel\",\"webhookUrl\":\"https://webhook.success\",\"products\":[{\"title\":\"10 Credits\",\"sku\":\"12345\",\"price\":100,\"quantity\":1,\"currencyCode\":\"NGN\"}],\"metadata\":[{\"key\":\"customerId\",\"value\":\"1\"}]}";
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(timestamp + body));
string signature = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); // must be lowercase
Console.WriteLine(signature);
}
}
}import hashlib
import hmac
signing_key = "hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y"
timestamp = "2025-03-17T08:10:52.544247646Z" # real request: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
body = '{"countryCode":"NG","currencyCode":"NGN","amount":100,"partnerReference":"4a662291-2840-4bcd-aae9-553ab24601ed","callbackSuccessUrl":"https://callback.success","callbackFailureUrl":"https://callback.failure","callbackPendingUrl":"https://callback.pending","callbackCancelUrl":"https://callback.cancel","webhookUrl":"https://webhook.success","products":[{"title":"10 Credits","sku":"12345","price":100,"quantity":1,"currencyCode":"NGN"}],"metadata":[{"key":"customerId","value":"1"}]}'
signature = hmac.new(
signing_key.encode("utf-8"),
(timestamp + body).encode("utf-8"),
hashlib.sha256,
).hexdigest() # lowercase hex
print(signature)const crypto = require("crypto");
const signingKey = "hCyO_Flnu6aid-bhFYTYOowkxXRzoZkgzO32rB6Ik8Y";
const timestamp = "2025-03-17T08:10:52.544247646Z"; // real request: new Date().toISOString()
const body = '{"countryCode":"NG","currencyCode":"NGN","amount":100,"partnerReference":"4a662291-2840-4bcd-aae9-553ab24601ed","callbackSuccessUrl":"https://callback.success","callbackFailureUrl":"https://callback.failure","callbackPendingUrl":"https://callback.pending","callbackCancelUrl":"https://callback.cancel","webhookUrl":"https://webhook.success","products":[{"title":"10 Credits","sku":"12345","price":100,"quantity":1,"currencyCode":"NGN"}],"metadata":[{"key":"customerId","value":"1"}]}';
const signature = crypto
.createHmac("sha256", signingKey)
.update(timestamp + body, "utf8")
.digest("hex"); // lowercase hex
console.log(signature);Troubleshooting a Rejected Signature
If a request is rejected, check these in order:
- Key and data are swapped — the Signing Key is the HMAC key, and
timestamp + bodyis the data. - Timestamp missing from the data — the data must start with the timestamp, not just the JSON.
- Body changed after signing — the signed body must be byte-for-byte the body sent. Common causes are trimming, pretty-printing, re-serialising the JSON, or an HTTP client that rewrites the body.
- Uppercase hex — the signature must be lowercase.
- Different timestamps —
X-Timestampmust be the exact string that was signed. - Timestamp format —
X-Timestampmust be an ISO-8601 date-time with aTbetween the date and the time, ending inZ(for example2025-03-17T08:10:52.544Z). Epoch milliseconds, or a date-time without a time zone, are not accepted. - Clock drift — the timestamp must be within the allowed window of the current UTC time.
- Key decoded or altered — use the Signing Key exactly as issued, without base64-decoding it.
If it still fails, send us the X-Timestamp, the request body and the X-Signature from one request (never the Signing Key) and we will check it on our side.
