Authentication
Sign exact request bytes and distinguish the two API authentication families.
Merchant HMAC
Send X-API-KEY-ID, X-API-TIMESTAMP (Unix seconds), a unique X-API-NONCE and X-API-SIGNATURE. For JSON POST requests also send Content-Type: application/json. The signature is lowercase hexadecimal HMAC-SHA256 over six LF-separated lines:
METHOD
PATH
QUERY_STRING
TIMESTAMP
NONCE
SHA256_HEX(RAW_BODY)
METHOD is uppercase. PATH includes resolved identifiers. QUERY_STRING excludes the question mark and preserves outgoing order and encoding. Hash the exact UTF-8 body; GET hashes the empty string, not {}.
import { createHash, createHmac, randomUUID } from 'node:crypto';
export function signedHeaders({ method, path, query = '', body = '', keyId, secret,
timestamp = String(Math.floor(Date.now() / 1000)), nonce = randomUUID().replaceAll('-', '') }) {
if (!keyId || !secret) throw new Error('Server-side API credentials are required');
if (path.includes('{') || path.includes('}')) throw new Error('Resolve path variables before signing');
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex');
const canonical = [method.toUpperCase(), path, query, timestamp, nonce, bodyHash].join('\n');
return {
'X-API-KEY-ID': keyId,
'X-API-TIMESTAMP': timestamp,
'X-API-NONCE': nonce,
'X-API-SIGNATURE': createHmac('sha256', secret).update(canonical, 'utf8').digest('hex'),
};
}
"""Server-side signing examples; no API requests are made by this module."""
import hashlib
import hmac
import time
import uuid
def signed_headers(*, method, path, key_id, secret, query='', body='', timestamp=None, nonce=None):
if not key_id or not secret:
raise ValueError('Server-side API credentials are required')
if '{' in path or '}' in path:
raise ValueError('Resolve path variables before signing')
timestamp = str(int(time.time())) if timestamp is None else str(timestamp)
nonce = uuid.uuid4().hex if nonce is None else nonce
body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
canonical = '\n'.join([method.upper(), path, query, timestamp, nonce, body_hash])
signature = hmac.new(secret.encode('utf-8'), canonical.encode('utf-8'), hashlib.sha256).hexdigest()
return {'X-API-KEY-ID': key_id, 'X-API-TIMESTAMP': timestamp,
'X-API-NONCE': nonce, 'X-API-SIGNATURE': signature}
def plugin_signed_headers(*, method, path, key_id, secret, body='', timestamp=None):
if not key_id or not secret:
raise ValueError('Server-side API credentials are required')
if '{' in path or '}' in path:
raise ValueError('Resolve path variables before signing')
timestamp = str(int(time.time())) if timestamp is None else str(timestamp)
canonical = f'{timestamp}.{method.upper()}.{path}.{body}'
signature = hmac.new(secret.encode('utf-8'), canonical.encode('utf-8'), hashlib.sha256).hexdigest()
return {'X-ZytePe-Api-Key': key_id, 'X-ZytePe-Timestamp': timestamp,
'X-ZytePe-Signature': signature}
Serialize a POST body once and pass that same string to signing and fetch. Regenerate timestamp/nonce/signature for each HTTP request. For a payout retry, retain the same business idempotency key.
Plugin signatures
Plugin operations use X-ZytePe-Api-Key, X-ZytePe-Timestamp and X-ZytePe-Signature. Their canonical string is separate:
timestamp.METHOD.PATH.raw_body
The reviewed local handler uses Unix seconds and a lowercase hexadecimal HMAC-SHA256 digest. GET uses an empty raw body. This clarifies omissions in the supplied contract; confirm the deployed handler matches before rollout. Do not reuse the six-line HMAC canonical string with plugin headers.
Webhook verification is separate
The webhook contract signs a JSON payload, not an API request canonical string. Never reuse either request-signing helper for incoming callbacks.
Troubleshoot signatures
Check the full secret, synchronized timestamp, fresh nonce, resolved path and exact body bytes. Pretty-printing JSON after signing changes the signature. Do not log secrets or raw personal data while diagnosing a failed request.
