Authorisation
All Pay.io API requests must be authenticated with three headers:
X-API-Key
Your unique merchant API key from Merchant Console.
X-API-Nonce
Unique identifier (UUID) per request.
X-API-Signature
RSA-SHA256 signature generated using your private key.
Creating a Public and Private Key
Merchants must provide Pay.io with a public key during onboarding.
Requirements:
At least 2048 bits
PEM format
You can use the following code sample to generate the public key and private key.
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# Generate 2048-bit private key
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# Serialize keys to PEM
pem_private = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
pem_public = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)Creating a Merchant Signature
Every API request must be signed.
Steps
Generate a nonce — Secure random string, at least 16 chars
# Example in Python def generate_nonce(): return str(uuid.uuid4())Build canonical string using the rules:
The request’s signature is calculated over this exact concatenation (no delimiters):
METHOD + PATH + NONCE + QUERY + BODY
Sign with your merchant’s private key using RSA-SHA256.
Base64-encode the signature.
Send it in the
X-API-Signatureheader
Example Python
def generate_auth_headers(method, path, query_string, body, api_signing_secret):
# 1. Generate unique nonce (UUID4 string)
nonce = str(uuid.uuid4())
# 2. Build the canonical signing data
signing_data = method + path + nonce + query_string + (body or "")
# 3. Create the HMAC-SHA256 signature
signature = hmac.new(
key=api_signing_secret.encode("utf-8"),
msg=signing_data.encode("utf-8"),
digestmod=hashlib.sha256
).hexdigest()
# 4. Return headers
return {
"X-API-Nonce": nonce,
"X-API-Signature": signature
}
# Example usage
headers = generate_auth_headers(
method="POST",
path="/v1/payments",
query_string="order_id=123",
body='{"amount":100,"currency":"USD"}',
api_signing_secret="my_secret_key"
)Example Authenticated Request
curl --location 'https://pgw.stage.pay.io/v1/user/withdraw' \
--header 'X-API-Key: YOUR_API_KEY' \
--header 'X-API-Nonce: 123e4567-e89b-12d3-a456-426614174000' \
--header 'X-API-Signature: BASE64_SIGNATURE' \
--data '{"amount":"100.50","currency_id":"c872e749-fd56-533e-b01f-de87ae38e7f1","wallet_address":"0x123...","user_reference_id":"hub_player_2"}'Error Codes
Error Type: missing signature
missing signatureStatus Code: 401
Response Body:
{ "message": "missing signature" }Error Type: missing api key
missing api keyStatus Code: 401
Response Body:
{ "message": "missing api key" }Error Type: invalid api key
invalid api keyStatus Code: 401
Response Body:
{ "message": "invalid api key" }Error Type: nonce too short
nonce too shortStatus Code: 400
Response Body:
{ "message": "nonce too short" }Error Type: nonce already used
nonce already usedStatus Code: 401
Response Body:
{ "message": "invalid request signature" }Error Type: invalid nonce
invalid nonceStatus Code: 400
Response Body:
{ "message": "invalid nonce" }Error Type: missing nonce
missing nonceStatus Code: 401
Response Body:
{ "message": "missing nonce" }Error Type: timestamp expired
timestamp expiredStatus Code: 401
Response Body:
{ "message": "timestamp expired" }Error Type: multiple nonces
multiple noncesStatus Code: 401
Response Body:
{ "message": "multiple nonces" }Last updated