> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lyseis-pay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Authenticate Paylink API Requests Securely

> Learn how to sign every Paylink API request with HMAC-SHA256 — including required headers, the signing algorithm, and how to handle auth errors.

Paylink uses an **HMAC-SHA256 signature scheme** to authenticate requests from your server. Instead of sending your secret directly in a header, you use it as a signing key to produce a cryptographic digest of each request. The server re-derives the same digest from the headers you send and rejects the request if the values do not match. This means your secret is never transmitted over the wire and a captured request cannot be replayed.

The only endpoint that does **not** require authentication is `GET /health`.

## Required Headers

Every protected request must include the following three headers.

<ParamField header="X-Key-Id" type="string" required>
  Your active merchant API key identifier. Retrieve this from the [Dashboard](https://dashboard.lyseis-pay.com) under **Settings → API Keys**. This value identifies *which* key pair was used to sign the request.
</ParamField>

<ParamField header="X-Timestamp" type="string" required>
  The current Unix timestamp **in seconds** as a decimal string (for example, `"1700000000"`). The server rejects requests whose timestamp is more than **5 minutes** in the past or future, protecting against replay attacks.
</ParamField>

<ParamField header="X-Signature" type="string" required>
  A lowercase hexadecimal HMAC-SHA256 digest that proves you hold the API secret. See [Signing Algorithm](#signing-algorithm) below for exactly how to compute this value.
</ParamField>

## Signing Algorithm

Build the message to sign by concatenating the timestamp, a colon, and the **raw request body**:

```text title="Signed message format" theme={null}
{X-Timestamp}:{raw_request_body}
```

For requests with no body (for example, `GET` requests), use an empty string as the body:

```text title="Signed message — no body" theme={null}
{X-Timestamp}:
```

Then compute the HMAC-SHA256 digest of that message using your **API secret** as the key, and encode the result as a **lowercase hex string**.

<CodeGroup>
  ```python title="Python" theme={null}
  import hashlib
  import hmac
  import time

  API_KEY_ID = "your_key_id_here"
  API_SECRET = "your_secret_here"

  body = '{"email":"customer@example.com","amount":"2500.00","currency":"NGN"}'
  timestamp = str(int(time.time()))
  message = f"{timestamp}:{body}".encode()
  signature = hmac.new(API_SECRET.encode(), message, hashlib.sha256).hexdigest()

  headers = {
      "Content-Type": "application/json",
      "X-Key-Id": API_KEY_ID,
      "X-Timestamp": timestamp,
      "X-Signature": signature,
  }
  ```

  ```javascript title="Node.js" theme={null}
  const crypto = require('crypto');

  const API_KEY_ID = process.env.LYSEIS_API_KEY_ID;
  const API_SECRET = process.env.LYSEIS_API_SECRET;

  const timestamp = Math.floor(Date.now() / 1000).toString();
  const body = JSON.stringify({ email: 'customer@example.com', amount: '2500.00', currency: 'NGN' });
  const message = `${timestamp}:${body}`;
  const signature = crypto.createHmac('sha256', API_SECRET).update(message).digest('hex');

  const headers = {
    'Content-Type': 'application/json',
    'X-Key-Id': API_KEY_ID,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
  };
  ```
</CodeGroup>

## Security Tips

<Tip>
  Sign the **exact bytes** you send as the request body. If you serialize the body to a string, sign that string, and then re-serialize or pretty-print before sending, the body bytes will change and the signature will be invalid. Build the body string once, sign it, and send it as-is.
</Tip>

<Warning>
  Never expose your **API secret** in client-side code, mobile app bundles, or public repositories. The secret must only exist on your backend server. If you suspect a secret has been compromised, rotate the key immediately in the [Dashboard](https://dashboard.lyseis-pay.com) — old keys are invalidated the moment you rotate.
</Warning>

## Authentication Errors

When authentication fails, the API returns a `401 Unauthorized` or `403 Forbidden` response with a machine-readable error message. The table below lists every possible auth error and what it means.

| HTTP Status | Error Message                  | Meaning                                                                                                                                                                                                                     |
| ----------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`       | `Invalid or missing API key`   | The `X-Key-Id` header is absent, does not match any active key on your account, or the key has been revoked.                                                                                                                |
| `401`       | `Invalid request timestamp`    | The `X-Timestamp` header is missing, not a valid integer, or cannot be parsed as a Unix timestamp.                                                                                                                          |
| `401`       | `Request timestamp is too old` | The timestamp in `X-Timestamp` is more than **5 minutes** behind the server clock. Ensure your server clock is synchronized (for example, via NTP).                                                                         |
| `401`       | `Invalid auth header`          | The `X-Signature` header is present but the computed digest does not match the server's expectation. This usually means the body was modified after signing, the wrong secret was used, or the message format is incorrect. |
| `401`       | `Invalid or expired token`     | A session token provided in the `Authorization` header is no longer valid. Re-authenticate to obtain a fresh token.                                                                                                         |
| `403`       | `Missing required capability`  | Your API key exists and the signature is valid, but the key does not have the permission scope required to call this endpoint. Update the key's capabilities in the Dashboard.                                              |
