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

# Authentication

> Authenticate with the Clopos Open API v2 using a JWT access token

# Authentication

The Clopos Open API v2 uses short-lived **JWT access tokens**. Exchange your credentials at `/v2/auth` to receive a token, then include it in the `x-token` header on every subsequent request.

## Prerequisites

Before you can authenticate you will need:

* **Client ID** — your application's unique identifier
* **Client Secret** — your application's secret key
* **Brand** — the brand identifier you want to access
* **Integrator ID** — identifies the integrator making the request. New in v2 and required on every auth call.

<Note>
  Request your credentials and an `integrator_id` from Clopos by filling out [this form](https://forms.gle/Y9P1Wnv4QFAruxny8).
</Note>

<Warning>
  All v2 requests use the base URL `https://integrations.clopos.com/open-api/v2`.
</Warning>

## Authentication flow

### Step 1: Obtain an access token

Send a `POST` request to the v2 auth endpoint:

```bash theme={null}
curl -X POST https://integrations.clopos.com/open-api/v2/auth \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "{{client_id}}",
    "client_secret": "{{client_secret}}",
    "brand": "{{brand}}",
    "integrator_id": "{{integrator_id}}"
  }'
```

**Request body**

| Field           | Type   | Required | Description                             |
| --------------- | ------ | -------- | --------------------------------------- |
| `client_id`     | string | Yes      | Issued by Clopos.                       |
| `client_secret` | string | Yes      | Paired secret for the client.           |
| `brand`         | string | Yes      | Brand identifier.                       |
| `integrator_id` | string | Yes      | Integrator identifier issued by Clopos. |

<Note>
  `venue_id` is **not** part of the v2 auth body. The active venue is resolved from the JWT and can optionally be overridden per-request with the `x-venue` header.
</Note>

### Step 2: Inspect the response

A successful authentication returns a signed JWT:

```json theme={null}
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": 1767852332,
  "message": "Authentication successful"
}
```

| Field        | Type    | Description                                           |
| ------------ | ------- | ----------------------------------------------------- |
| `token`      | string  | The JWT. Include it verbatim in the `x-token` header. |
| `token_type` | string  | Always `Bearer`.                                      |
| `expires_in` | integer | Token lifetime in seconds (typically `3600`).         |
| `expires_at` | integer | Unix timestamp after which the token is rejected.     |

The JWT encodes your `brand`, `venue_id`, `integrator_id`, and upstream auth state, so you do **not** need to send them as separate headers.

### Step 3: Call an authenticated endpoint

Include the JWT in the `x-token` header. That is the only header required on v2 endpoints.

```bash theme={null}
curl -X GET "https://integrations.clopos.com/open-api/v2/products" \
  -H "x-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```

**Required header**

* `x-token` — the JWT returned by `/v2/auth`.

**Optional header**

* `x-venue` — override the venue encoded in the JWT for this request. Useful when a single integrator operates across multiple venues.

## Token management

<Warning>
  Tokens expire after `expires_in` seconds (typically **1 hour**). Refresh them before they expire to avoid request failures.
</Warning>

### Best practices

1. **Store tokens securely** — never expose them in client-side code or commit them to source control.
2. **Refresh proactively** — re-authenticate before `expires_at` rather than waiting for a `401`.
3. **Handle errors gracefully** — on any `401`, re-authenticate and retry once.
4. **Use HTTPS only** — never send credentials over unencrypted connections.

## Error handling

### Errors from `/v2/auth`

| Status             | Body                                                                                                                              | Cause                                                                                                                                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`  | `{ "success": false, "error": "Missing client_id, client_secret, brand, or integrator_id" }`                                      | One or more required fields were omitted.                                                                                                                                                             |
| `400 Bad Request`  | `{ "success": false, "error": "Invalid integrator_id" }`                                                                          | The `integrator_id` is unknown or inactive.                                                                                                                                                           |
| `200 OK`           | `{ "success": false, "error": "Integrator is in test mode. But brand is not in test mode", "brand": "...", "integrator": "..." }` | The integrator is flagged as test-only, but the brand is in production stage. Switch to a production integrator or a test brand. Note this is returned as a `200` with `success: false`, not a `4xx`. |
| `401 Unauthorized` | `{ "success": false, "message": "..." }`                                                                                          | Upstream rejected the `client_id` / `client_secret` / `brand` combination.                                                                                                                            |

### Errors from authenticated endpoints

| Status             | Body                                                                                                                              | Cause                                                                                                                                                            |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | `{ "success": false, "error": "Headers are missing" }`                                                                            | The `x-token` header was not sent.                                                                                                                               |
| `401 Unauthorized` | `{ "success": false, "error": "Invalid token" }`                                                                                  | The JWT is malformed or its signature does not verify.                                                                                                           |
| `401 Unauthorized` | `{ "success": false, "expires_at": "2026-01-01T12:00:00.000Z", "error": "Token expired" }`                                        | The JWT expired. Re-authenticate to get a new one. Note that `expires_at` in this error is an ISO 8601 string, unlike the Unix timestamp returned by `/v2/auth`. |
| `401 Unauthorized` | `{ "success": false, "error": "Invalid integrator_id" }`                                                                          | The `integrator_id` encoded in the token is no longer active.                                                                                                    |
| `401 Unauthorized` | `{ "success": false, "error": "Integrator is in test mode. But brand is not in test mode", "brand": "...", "integrator": "..." }` | Same test/production mismatch as above, enforced on every authenticated call.                                                                                    |

## Code examples

<CodeGroup>
  ```javascript JavaScript theme={null}
  const BASE_URL = 'https://integrations.clopos.com/open-api/v2';

  async function authenticate() {
    const response = await fetch(`${BASE_URL}/auth`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: '{{client_id}}',
        client_secret: '{{client_secret}}',
        brand: '{{brand}}',
        integrator_id: '{{integrator_id}}'
      })
    });

    const data = await response.json();

    if (!data.success) {
      throw new Error(data.error || 'Authentication failed');
    }

    return { token: data.token, expiresAt: data.expires_at };
  }

  async function makeAuthenticatedRequest(path, token, options = {}) {
    return fetch(`${BASE_URL}${path}`, {
      ...options,
      headers: {
        'x-token': token,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
  }
  ```

  ```python Python theme={null}
  import requests
  import time

  BASE_URL = "https://integrations.clopos.com/open-api/v2"


  class CloposAPI:
      def __init__(self, client_id, client_secret, brand, integrator_id):
          self.client_id = client_id
          self.client_secret = client_secret
          self.brand = brand
          self.integrator_id = integrator_id
          self.token = None
          self.expires_at = 0

      def authenticate(self):
          response = requests.post(
              f"{BASE_URL}/auth",
              json={
                  "client_id": self.client_id,
                  "client_secret": self.client_secret,
                  "brand": self.brand,
                  "integrator_id": self.integrator_id,
              },
          )
          data = response.json()

          if not data.get("success"):
              raise Exception(data.get("error") or "Authentication failed")

          self.token = data["token"]
          self.expires_at = data["expires_at"]
          return self.token

      def _ensure_token(self):
          if not self.token or time.time() >= self.expires_at - 60:
              self.authenticate()

      def request(self, method, path, **kwargs):
          self._ensure_token()
          headers = kwargs.pop("headers", {})
          headers["x-token"] = self.token
          return requests.request(method, f"{BASE_URL}{path}", headers=headers, **kwargs)
  ```

  ```php PHP theme={null}
  <?php
  class CloposAPI {
      private string $base_url = "https://integrations.clopos.com/open-api/v2";
      private ?string $token = null;
      private int $expires_at = 0;

      public function __construct(
          private string $client_id,
          private string $client_secret,
          private string $brand,
          private string $integrator_id
      ) {}

      public function authenticate(): string {
          $payload = json_encode([
              "client_id" => $this->client_id,
              "client_secret" => $this->client_secret,
              "brand" => $this->brand,
              "integrator_id" => $this->integrator_id,
          ]);

          $context = stream_context_create([
              "http" => [
                  "method" => "POST",
                  "header" => "Content-Type: application/json\r\n",
                  "content" => $payload,
                  "ignore_errors" => true,
              ],
          ]);

          $response = json_decode(file_get_contents($this->base_url . "/auth", false, $context), true);

          if (empty($response["success"])) {
              throw new Exception($response["error"] ?? "Authentication failed");
          }

          $this->token = $response["token"];
          $this->expires_at = $response["expires_at"];
          return $this->token;
      }

      public function request(string $method, string $path, ?array $body = null): array {
          if (!$this->token || time() >= $this->expires_at - 60) {
              $this->authenticate();
          }

          $options = [
              "http" => [
                  "method" => $method,
                  "header" => "x-token: {$this->token}\r\nContent-Type: application/json\r\n",
                  "ignore_errors" => true,
              ],
          ];
          if ($body !== null) {
              $options["http"]["content"] = json_encode($body);
          }

          $context = stream_context_create($options);
          return json_decode(file_get_contents($this->base_url . $path, false, $context), true);
      }
  }
  ```
</CodeGroup>

## Next steps

Once you have your access token you can start making API requests:

* [List products](/api-reference/v2/products/get-all-products)
* [Create an order](/api-reference/v2/orders/create-order)
* [Manage receipts](/api-reference/v2/receipts/get-receipts)
