# Authorisation

All Pay.io API requests must be authenticated with three headers:

<table><thead><tr><th width="187.12933349609375">Header</th><th>Description</th></tr></thead><tbody><tr><td><code>X-API-Key</code></td><td>Your unique merchant API key from Merchant Console.</td></tr><tr><td><code>X-API-Nonce</code></td><td>Unique identifier (UUID) per request.</td></tr><tr><td><code>X-API-Signature</code></td><td>RSA-SHA256 signature generated using your private key.</td></tr></tbody></table>

***

### 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.

{% code title="Example in Python" overflow="wrap" lineNumbers="true" %}

```python
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
)
```

{% endcode %}

***

### Creating a Merchant Signature

Every API request must be signed.

#### Steps

1. **Generate a nonce** — Secure random string, at least 16 chars

   `# Example in Python def generate_nonce(): return str(uuid.uuid4())`
2. **Build canonical string** using the rules:
   1. The request’s signature is calculated over this **exact** concatenation (no delimiters):

      `METHOD + PATH + NONCE + QUERY + BODY`
3. **Sign** with your merchant’s private key using **RSA-SHA256**.
4. **Base64-encode** the signature.
5. **Send** it in the `X-API-Signature` header

#### Example Python&#x20;

{% code overflow="wrap" lineNumbers="true" %}

```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"
)
```

{% endcode %}

***

### Example Authenticated Request

{% code overflow="wrap" lineNumbers="true" %}

```bash
curl --location 'https://gateway.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"}'
```

{% endcode %}

***

### Error Codes&#x20;

#### Error Type:  `missing signature` <a href="#error-type-missing-signature" id="error-type-missing-signature"></a>

Status Cod&#x65;**:** `401`

Response Body:

```json
{ "message": "missing signature" }
```

#### Error Type: `missing api key` <a href="#error-type-missing-api-key" id="error-type-missing-api-key"></a>

Status Code: `401`

Response Body:

```json
{ "message": "missing api key" }
```

#### Error Type: `invalid api key` <a href="#error-type-invalid-api-key" id="error-type-invalid-api-key"></a>

Status Code: `401`

Response Body:

```json
{ "message": "invalid api key" }
```

#### Error Type: `nonce too short` <a href="#error-type-nonce-too-short" id="error-type-nonce-too-short"></a>

Status Code: `400`

Response Body:

```json
{ "message": "nonce too short" }
```

#### **Error Type: `nonce already used`** <a href="#error-type-nonce-already-used" id="error-type-nonce-already-used"></a>

Status Code: `401`

Response Body:

```json
{ "message": "invalid request signature" }
```

#### **Error Type: `invalid nonce`** <a href="#error-type-invalid-nonce" id="error-type-invalid-nonce"></a>

* **Status Code: 400**
* **Response Body:**

```json
{ "message": "invalid nonce" }
```

#### **Error Type: `missing nonce`** <a href="#error-type-missing-nonce" id="error-type-missing-nonce"></a>

Status Cod&#x65;**:** `401`

Response Body:

```json
{ "message": "missing nonce" }
```

#### **Error Type: `timestamp expired`** <a href="#error-type-timestamp-expired" id="error-type-timestamp-expired"></a>

Status Code: `401`

Response Body:

```json
{ "message": "timestamp expired" }
```

#### **Error Type: `multiple nonces`** <a href="#error-type-multiple-nonces" id="error-type-multiple-nonces"></a>

Status Code: `401`

Response Body:

```json
{ "message": "multiple nonces" }
```
