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

# Authentication

> Get an access token, include it in every request, and handle token refresh and access keys for webhook signing.

Every request to the TagPay API (except merchant registration) requires a valid access token. This page explains how to get a token, how to use it, when it expires, and how to handle expiry gracefully.

## Get an access token

Call `POST /auth/login` with your merchant account email and password. The API returns tokens in **response headers**, not in the response body.

```
POST https://api.tagpay.ng/v1/auth/login
```

<Warning>
  The email and password values must be **Base64-encoded** before sending. The server decodes them server-side before validation. Example: `Buffer.from("your@email.com").toString("base64")` in Node.js, or `echo -n "your@email.com" | base64` in bash.
</Warning>

### Request

<ParamField body="email" type="string" required>
  Your merchant account email address, **Base64-encoded**.
</ParamField>

<ParamField body="password" type="string" required>
  Your account password (minimum 6 characters), **Base64-encoded**.
</ParamField>

<ParamField body="metadata" type="object">
  Optional device context for audit logging. Useful for identifying sessions.

  <Expandable title="metadata fields">
    <ParamField body="metadata.deviceFingerprint" type="string">
      A unique fingerprint for the device making the request. Maximum 255 characters.
    </ParamField>

    <ParamField body="metadata.deviceId" type="string">
      A stable identifier for the device. Maximum 255 characters.
    </ParamField>

    <ParamField body="metadata.timezone" type="string">
      The device timezone, e.g. `Africa/Lagos`. Maximum 100 characters.
    </ParamField>

    <ParamField body="metadata.language" type="string">
      The device locale, e.g. `en-NG`. Maximum 20 characters.
    </ParamField>
  </Expandable>
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  # Base64-encode your credentials first:
  # echo -n "ada@example.com" | base64   →  YWRhQGV4YW1wbGUuY29t
  # echo -n "securepassword" | base64    →  c2VjdXJlcGFzc3dvcmQ=

  curl -X POST https://api.tagpay.ng/v1/auth/login \
    -H "Content-Type: application/json" \
    -D - \
    -d '{
      "email": "YWRhQGV4YW1wbGUuY29t",
      "password": "c2VjdXJlcGFzc3dvcmQ="
    }'
  ```

  ```javascript Node.js theme={null}
  const email = Buffer.from("ada@example.com").toString("base64");
  const password = Buffer.from("securepassword").toString("base64");

  const response = await fetch("https://api.tagpay.ng/v1/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });

  const accessToken = response.headers.get("X-Access-Token");
  const refreshToken = response.headers.get("X-Refresh-Token");
  const data = await response.json();
  ```
</CodeGroup>

### Response

The access and refresh tokens are in the **response headers**.

| Header            | Description                                                            |
| ----------------- | ---------------------------------------------------------------------- |
| `X-Access-Token`  | Bearer token to include on all subsequent requests                     |
| `X-Refresh-Token` | Token used to generate a new access token when the current one expires |

**Response body**

```json theme={null}
{
  "status": true,
  "data": {
    "id": "usr_01hw...",
    "role": "MERCHANT",
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Obi",
    "mode": "SANDBOX",
    "createdAt": "2026-04-08T10:00:00.000Z",
    "updatedAt": "2026-04-08T10:00:00.000Z"
  },
  "merchant": {
    "id": "mer_01hw..",
    "businessName": "Ada Ventures",
    "businessType": "FINANCIAL-SERVICES",
    "mode": "SANDBOX",
    "review": "PENDING",
    "owner": true,
    "role": "MERCHANT",
    "callbackURL": null,
    "sandboxCallbackURL": null,
    "parentMerchant": null,
    "canDebitCustomer": false
  },
  "availableMerchants": [],
  "hasMultipleMerchants": false
}
```

<Note>
  If your account has not been verified, the login response will include `"requiresVerification": true` and the API will send a new verification code to your email. Verify your account using `PUT /merchant/verify` before attempting to log in again.
</Note>

## Authenticate requests

Include the access token in the `Authorization` header on every request:

```
Authorization: Bearer <your-access-token>
```

<CodeGroup>
  ```bash curl theme={null}
  curl -X GET https://api.tagpay.ng/v1/merchant/wallet \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tagpay.ng/v1/merchant/wallet", {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  ```
</CodeGroup>

## Refresh your token

Access tokens expire. When a request returns `401 Unauthorized`, use your refresh token to obtain a new token pair without asking the user to log in again.

```
POST https://api.tagpay.ng/v1/auth/refresh/token
```

Pass the refresh token in the `X-Refresh-Token` request header (not in the body).

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.tagpay.ng/v1/auth/refresh/token \
    -H "X-Refresh-Token: d4f9a2c1e8b3..."
  ```

  ```javascript Node.js theme={null}
  async function refreshAccessToken(refreshToken) {
    const response = await fetch("https://api.tagpay.ng/v1/auth/refresh/token", {
      method: "POST",
      headers: { "X-Refresh-Token": refreshToken },
    });

    const newAccessToken = response.headers.get("X-Access-Token");
    const newRefreshToken = response.headers.get("X-Refresh-Token");

    return { newAccessToken, newRefreshToken };
  }
  ```
</CodeGroup>

The API **rotates** the token pair on every refresh. Store the new `X-Access-Token` and `X-Refresh-Token` from the response headers and discard the old ones.

**Response headers (on success)**

```
X-Access-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Refresh-Token: a1b2c3d4e5f6...
```

<Warning>
  Each refresh token can only be used once. Once you call `/auth/refresh/token`, the previous token pair is invalidated. If your refresh token is expired or invalid, the user must log in again.
</Warning>

### Automatic token refresh in Node.js

```javascript theme={null}
class TagPayClient {
  constructor({ accessToken, refreshToken }) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
  }

  async request(url, options = {}) {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${this.accessToken}`,
      },
    });

    if (response.status === 401) {
      // Token expired — refresh and retry once
      const refreshed = await fetch("https://api.tagpay.ng/v1/auth/refresh/token", {
        method: "POST",
        headers: { "X-Refresh-Token": this.refreshToken },
      });

      if (!refreshed.ok) throw new Error("Session expired. Please log in again.");

      this.accessToken = refreshed.headers.get("X-Access-Token");
      this.refreshToken = refreshed.headers.get("X-Refresh-Token");

      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${this.accessToken}`,
        },
      });
    }

    return response;
  }
}
```

## Access keys (for webhook signing)

In addition to your access token, TagPay gives you a **public key** and **private key** for your merchant account. These are used to sign and verify webhook payloads — they are not used for API authentication.

### Get your access keys

```
GET https://api.tagpay.ng/v1/merchant/my-access-keys
```

<CodeGroup>
  ```bash curl theme={null}
  curl -X GET https://api.tagpay.ng/v1/merchant/my-access-keys \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tagpay.ng/v1/merchant/my-access-keys", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  const { data } = await response.json();
  console.log("Public key:", data.publicKey);
  console.log("Private key:", data.privateKey);
  ```
</CodeGroup>

**Response**

```json theme={null}
{
  "status": true,
  "data": {
    "publicKey": "pk_sandbox_...",
    "privateKey": "sk_sandbox_..."
  }
}
```

<Warning>
  Your private key is sensitive. Do not expose it in client-side code or commit it to version control. Store it as an environment variable or in a secrets manager.
</Warning>

### Rotate your access keys

If your private key is compromised, generate a new key pair immediately. The old key pair is invalidated as soon as you generate new ones.

```
POST https://api.tagpay.ng/v1/merchant/generate-access-keys
```

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.tagpay.ng/v1/merchant/generate-access-keys \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.tagpay.ng/v1/merchant/generate-access-keys", {
    method: "POST",
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  const { data } = await response.json();
  // Update your stored keys immediately
  ```
</CodeGroup>

**Response**

```json theme={null}
{
  "status": true,
  "data": {
    "publicKey": "pk_sandbox_new...",
    "privateKey": "sk_sandbox_new..."
  }
}
```

## Common authentication errors

| Status | Code             | Meaning                                                                                                                                    |
| ------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | `UNAUTHORIZED`   | The access token is missing, expired, or invalid. Refresh the token or log in again.                                                       |
| `403`  | `FORBIDDEN`      | The token is valid but your account does not have permission for this action. Check that your account is active and has the required role. |
| `400`  | `ACCOUNT_LOCKED` | Your merchant account has been locked from login. Contact TagPay support to resolve this.                                                  |

<Tip>
  Always check for a `401` response in your HTTP client and implement automatic token refresh. Users should never see an authentication error if your refresh logic is in place.
</Tip>
