Signature Algorithm
Algorithm Description
Fields participating in the signature:
apiPath: Request URI path (required), excluding host, query string, and the/crypto-paymentprefix, e.g./payin/v1/createPaymentbody: Raw JSON string of the request body (required), byte-for-byte identical to what is sent to Paydifyx-api-key: Same asappId(required)x-api-timestamp: Current timestamp in milliseconds (required)
Note: URL query string parameters are not included in the signature. Make sure your signing content contains only the 4 fields above. For this reason, never pass sensitive business parameters via the query string — put them in the signed request
bodyinstead.
Steps:
- Put the 4 fields above into a
contentMapand sort entries bykeyin ascending order - JSON-serialize the
contentMapinto acontentstring - Compute
HMAC-SHA256(content, apiSecret) - Base64-encode the HMAC output — that is the signature string
Example:
curl -H 'x-api-key: key' \ -H 'x-api-timestamp: 1744636844000' \ -H 'x-api-signature: 0xb7Kxxxx=' \ 'https://{HOST}/path/to/pay' \ -d '{"data":"test"}'
After sorting and serialization, contentMap looks like this (note: body is a raw string, not an object):
{ "apiPath": "/path/to/pay", "body": "{\"data\":\"test\"}", "x-api-key": "key", "x-api-timestamp": "1744636844000" }
HMAC-SHA256 this JSON string with apiSecret and Base64-encode the result to get the signature.
QR Pay Signature Differences
The signature algorithm above applies to Payin / Payout, which pass signature via headers:
x-api-keyx-api-timestampx-api-signature
QR Pay and Fusion Pay use a different set of headers and signing mechanism:
X-DAPI-API-KEY— API key provided by PaydifyX-DAPI-TIMESTAMP— Request timestamp in millisecondsX-DAPI-NONCE— Random number for replay attack preventionX-DAPI-SIGN— Request signature (HMAC-SHA256 lowercase hex)
See Code Implementation · X-DAPI below.
Code Implementation
Payin / Payout Backend Examples
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"sort"
)
func GenSign(appId, apiSecret, pathUrl, payload string, tm int64) string {
// Parse URL
path := ""
contentMap := make(map[string]string)
if u, err := url.Parse(pathUrl); err == nil {
path = u.Path
for k, v := range u.Query() {
if len(v) > 0 {
contentMap[k] = v[0]
}
}
}
// Build the map to be signed
contentMap["x-api-key"] = appId
contentMap["x-api-timestamp"] = fmt.Sprintf("%d", tm) // milliseconds
contentMap["apiPath"] = path
contentMap["body"] = payload
// Get all keys and sort
keys := make([]string, 0, len(contentMap))
for k := range contentMap {
keys = append(keys, k)
}
sort.Strings(keys)
// Create ordered map
orderedMap := make(map[string]string)
for _, k := range keys {
orderedMap[k] = contentMap[k]
}
// JSON serialization
content, _ := json.Marshal(orderedMap)
// Calculate HMAC
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write(content)
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
// Usage example
GenSign("A123456", "ABC123", "/path/to/pay?param1=test1¶m2=test2", "{"data":"test"}", time.Now().UnixMilli())QR Pay / Fusion Pay (X-DAPI) Backend Examples
Signing string:
dataToSign = apiKey + timestamp + nonce + queryParamsJson + requestBodyJson
signature = HMAC-SHA256(dataToSign, secretKey) → lowercase hex string
- Use
queryParamsJson = "{}"when the URL has no query string requestBodyJsonmust match the wire body, with null-valued fields recursively removed before signing- Set headers:
X-DAPI-API-KEY,X-DAPI-TIMESTAMP,X-DAPI-NONCE,X-DAPI-SIGN
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
)
// GenQrSign generates X-DAPI-SIGN for QR Pay / Fusion Pay.
// queryParamsJson: JSON string of URL query params; use "{}" when none.
// requestBodyJson: request body JSON after stripping null-valued fields.
func GenQrSign(apiKey, apiSecret, queryParamsJson, requestBodyJson, nonce string, timestamp int64) string {
if queryParamsJson == "" {
queryParamsJson = "{}"
}
dataToSign := fmt.Sprintf("%s%d%s%s%s", apiKey, timestamp, nonce, queryParamsJson, requestBodyJson)
mac := hmac.New(sha256.New, []byte(apiSecret))
mac.Write([]byte(dataToSign))
return hex.EncodeToString(mac.Sum(nil))
}
// QueryParamsJSON converts URL query params to the JSON string used in signing.
func QueryParamsJSON(rawURL string) (string, error) {
u, err := url.Parse(rawURL)
if err != nil {
return "{}", err
}
q := u.Query()
if len(q) == 0 {
return "{}", nil
}
m := make(map[string]interface{}, len(q))
for k, vs := range q {
if len(vs) == 1 {
m[k] = vs[0]
} else {
m[k] = vs
}
}
b, err := json.Marshal(m)
return string(b), err
}
// StripNullFields recursively removes null-valued fields to match server verification.
func StripNullFields(raw string) (string, error) {
if raw == "" {
return "", nil
}
var v interface{}
if err := json.Unmarshal([]byte(raw), &v); err != nil {
return raw, nil
}
cleaned := filterNull(v)
b, err := json.Marshal(cleaned)
return string(b), err
}
func filterNull(v interface{}) interface{} {
switch x := v.(type) {
case map[string]interface{}:
m := make(map[string]interface{}, len(x))
for k, val := range x {
if val == nil {
continue
}
if filtered := filterNull(val); filtered != nil {
m[k] = filtered
}
}
return m
case []interface{}:
arr := make([]interface{}, 0, len(x))
for _, item := range x {
if item == nil {
continue
}
if filtered := filterNull(item); filtered != nil {
arr = append(arr, filtered)
}
}
return arr
default:
return v
}
}
// Example: POST /qr/v1/scanpay (no query)
func ExampleScanpaySign() (string, error) {
apiKey := "your_api_key"
apiSecret := "your_api_secret"
timestamp := int64(1740000000000)
nonce := "12345"
queryParamsJSON := "{}"
rawBody := `{"codeValue":"pay://example","customerId":"C1","clientIP":"127.0.0.1","extInfo":null}`
bodyForSign, err := StripNullFields(rawBody)
if err != nil {
return "", err
}
return GenQrSign(apiKey, apiSecret, queryParamsJSON, bodyForSign, nonce, timestamp), nil
}Postman Pre-request Script (Payin / Payout)
Paste the following script into the Pre-request Script tab for Payin / Payout:
const appId = 'YOUR_APP_ID'; const apiSecret = 'YOUR_API_SECRET'; const ts = Date.now(); pm.request.headers.upsert({ key: 'x-api-key', value: appId }); pm.request.headers.upsert({ key: 'x-api-timestamp', value: String(ts) }); const path = pm.request.url.getPath(); const rawBody = pm.request.body && pm.request.body.raw ? pm.request.body.raw : ''; const bodyForSign = rawBody.replace(/\r?\n/g, ''); const ordered = { apiPath: path, body: bodyForSign, 'x-api-key': appId, 'x-api-timestamp': String(ts), }; const content = JSON.stringify(ordered); console.log('Signature content:', ordered); const signature = CryptoJS.HmacSHA256(content, apiSecret).toString( CryptoJS.enc.Base64 ); pm.request.headers.upsert({ key: 'x-api-signature', value: signature }); console.log('Generated signature:', signature);
Postman Pre-request Script (QR Pay / Fusion Pay)
const apiKey = 'YOUR_API_KEY'; const apiSecret = 'YOUR_API_SECRET'; const ts = Date.now(); const nonce = String(Math.floor(Math.random() * 90000) + 10000); pm.request.headers.upsert({ key: 'X-DAPI-API-KEY', value: apiKey }); pm.request.headers.upsert({ key: 'X-DAPI-TIMESTAMP', value: String(ts) }); pm.request.headers.upsert({ key: 'X-DAPI-NONCE', value: nonce }); const params = {}; pm.request.url.query.each((q) => { if (q.disabled) return; params[q.key] = q.value; }); const queryParamsJson = Object.keys(params).length === 0 ? '{}' : JSON.stringify(params); const rawBody = pm.request.body && pm.request.body.raw ? pm.request.body.raw : ''; let requestBodyJson = rawBody.replace(/\r?\n/g, ''); const stripNull = (value) => { if (value === null || value === undefined) return undefined; if (Array.isArray(value)) { return value .map(stripNull) .filter((item) => item !== null && item !== undefined); } if (typeof value === 'object') { const out = {}; Object.keys(value).forEach((key) => { const next = stripNull(value[key]); if (next !== null && next !== undefined) out[key] = next; }); return out; } return value; }; if (requestBodyJson) { try { requestBodyJson = JSON.stringify(stripNull(JSON.parse(requestBodyJson))); } catch (e) { // keep raw body when not valid JSON } } const dataToSign = apiKey + ts + nonce + queryParamsJson + requestBodyJson; const signature = CryptoJS.HmacSHA256(dataToSign, apiSecret).toString( CryptoJS.enc.Hex ); pm.request.headers.upsert({ key: 'X-DAPI-SIGN', value: signature }); console.log('QR dataToSign:', dataToSign); console.log('Generated X-DAPI-SIGN:', signature);