> For the complete documentation index, see [llms.txt](https://docs.paykassma.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.paykassma.io/documentation/guides/authentication.md).

# Authentication

Every request to Paykassma and every postback we send is authenticated with an HMAC-SHA-512 signature. There are no OAuth tokens, no bearer headers — just a shared secret and a deterministic signing algorithm.

### Keys and secrets <a href="#keys-and-secrets" id="keys-and-secrets"></a>

* `api_key` — used to sign every request your backend sends to Paykassma, and to validate responses.
* `postback_key` — used by Paykassma to sign postbacks. Your server uses it to verify inbound webhooks.

{% hint style="warning" %}
These secrets are delivered during onboarding. Store them in a secrets manager. Never log them, never embed them in a mobile or web client.
{% endhint %}

### Signature algorithm <a href="#signature-algorithm" id="signature-algorithm"></a>

The signing procedure is fully deterministic:

1. Walk every key/value pair in the request body recursively. For lists, use the index as the key.
2. Build a string `key:value` for each leaf value and join them with `;`.
3. UTF-8 encode the joined string and the key.
4. Compute HMAC-SHA-512.
5. Base64-encode the digest. This is the `signature` field.

### Python reference implementation <a href="#python-reference-implementation" id="python-reference-implementation"></a>

```http
def _stringify(data: Any) -> Any:
    if data is None:
        return "null"

    if type(data) is bool:
        return "true" if data else "false"

    return data


def _flatten(data: Any, prefix: str = "") -> list[str]:
    items = []

    if isinstance(data, dict):
        for key, value in data.items():
            new_prefix = f"{prefix}:{key}" if prefix else key
            items.extend(_flatten(value, new_prefix))
    elif isinstance(data, list):
        for index, value in enumerate(data):
            new_prefix = f"{prefix}:{index}"
            items.extend(_flatten(value, new_prefix))
    else:
        items.append(f"{prefix}:{_stringify(data)}")

    return items


def generate_signature(data: dict[str, Any], api_key: str) -> str:
    if 'general' in data and 'signature' in data['general']:
        data['general'].pop('signature')

    message_bytes = ';'.join(sorted(_flatten(data))).encode('utf-8')
    secret_bytes = api_key.encode('utf-8')
    hmac_digest = hmac.new(secret_bytes, message_bytes, hashlib.sha512).digest()
    return base64.b64encode(hmac_digest).decode("utf-8")


def validate_signature(data: dict[str, Any], api_key: str) -> bool:
    provided_signature = data['general'].pop('signature')
    signature = generate_signature(data, api_key)

    logger.info("For request data (without general.signature): {} | Generated signature: {}", f'{data}', signature)
    return signature == provided_signature
```

### Verifying postback signatures <a href="#verifying-postback-signatures" id="verifying-postback-signatures"></a>

When you receive a postback, strip the `signature` field from the body, recompute the signature with your `postback_key`, and compare with `hmac.compare_digest`. Never compare with `==` — it’s vulnerable to timing attacks.

```http
import hmac
 
def verify_postback(body: dict, received_signature: str, postback_key: str) -> bool:
    body_no_sig = {k: v for k, v in body.items() if k != "signature"}
    expected = make_signature(body_no_sig, postback_key)
    return hmac.compare_digest(expected, received_signature)
```
