> For the complete documentation index, see [llms.txt](https://docs.pay.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.pay.io/api-reference/core-concepts/authorisation.md).

# Authorisation

## Authorisation

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

| Header            | Description                                            |
| ----------------- | ------------------------------------------------------ |
| `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.

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

***

### Creating a Merchant Signature

Every API request must be signed with your merchant's RSA private key. Pay.io verifies the signature using the public key you provided during onboarding. This is an asymmetric scheme — there is no shared secret.

Steps:

{% stepper %}
{% step %}
Generate a nonce - a secure random string of at least 16 characters. A UUID4 works well.
{% endstep %}

{% step %}
Normalize the body - strip all whitespace from the request body before signing. The server silently normalizes the body the same way, so signing pretty-printed JSON without stripping whitespace will fail signature verification.
{% endstep %}

{% step %}
Build the canonical string using the rule below (no delimiters): `METHOD + PATH + NONCE + QUERY + BODY`
{% endstep %}

{% step %}
Sign the canonical string with your merchant's private key using RSA-SHA256 (PKCS#1 v1.5).
{% endstep %}

{% step %}
Base64-encode the signature (standard Base64, not URL-safe).
{% endstep %}

{% step %}
Send the request with all three headers: `X-API-Key`, `X-API-Nonce`, and `X-API-Signature`.
{% endstep %}
{% endstepper %}

**Example in Python:**

```python
import uuid
import re
import base64
import json
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def generate_auth_headers(method, path, query_string, body, api_key, private_key_pem):
    # Load the merchant's RSA private key from PEM
    private_key = serialization.load_pem_private_key(
        private_key_pem.encode("utf-8") if isinstance(private_key_pem, str) else private_key_pem,
        password=None,
    )

    # Generate a unique nonce per request (UUID4, >= 16 chars)
    nonce = str(uuid.uuid4())

    # Strip all whitespace from the body to match server-side normalization
    normalized_body = re.sub(r"\s", "", body or "")

    # Build the canonical signing string: METHOD + PATH + NONCE + QUERY + BODY
    signing_data = f"{method.upper()}{path}{nonce}{query_string or ''}{normalized_body}"

    # Sign with RSA-SHA256 (PKCS#1 v1.5)
    signature = private_key.sign(
        signing_data.encode("utf-8"),
        padding.PKCS1v15(),
        hashes.SHA256(),
    )

    # Base64-encode the signature (standard, not URL-safe)
    signature_b64 = base64.b64encode(signature).decode("ascii")

    # Return the three required headers
    return {
        "X-API-Key": api_key,
        "X-API-Nonce": nonce,
        "X-API-Signature": signature_b64,
    }


# Example usage
headers = generate_auth_headers(
    method="POST",
    path="/v1/user/withdraw",
    query_string="",
    body=json.dumps({
        "amount": "100.50",
        "currency_id": "c872e749-...",
        "user_reference_id": "hub_player_2",
    }),
    api_key="YOUR_API_KEY",
    private_key_pem=open("merchant_private_key.pem", "rb").read(),
)
```

***

### Example Authenticated Request

```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"
}'
```

***

### Error Codes

**`missing signature`** — Status 401

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

**`missing api key`** — Status 401

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

**`invalid api key`** — Status 401

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

**`nonce too short`** — Status 400

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

**`nonce already used`** — Status 401

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

**`invalid nonce`** — Status 400

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

**`missing nonce`** — Status 401

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

**`timestamp expired`** — Status 401

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

**`multiple nonces`** — Status 401

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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.pay.io/api-reference/core-concepts/authorisation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
