> ## Documentation Index
> Fetch the complete documentation index at: https://developer.clopos.com/docs/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** — identifies the integration inside a single brand
* **Client Secret** — the secret paired with that Client ID
* **Brand** — the brand identifier of the Clopos customer you are integrating with
* **Integrator ID** — identifies you, the integrator. New in v2 and required on every auth call.

These come from two different places.

### Where the Integrator ID comes from

Request an `integrator_id` from Clopos by filling out [this form](https://forms.gle/Y9P1Wnv4QFAruxny8). You need one `integrator_id` for your integration as a whole, and you reuse it across every brand you connect to.

### Where the Client ID and Client Secret come from

The Client ID and Client Secret are **not** issued by Clopos. They are generated by the Clopos customer (the brand owner) inside their own back office, in the **Open API** module, and then shared with you along with their `brand` identifier.

<Info>
  If you are the integrator, send the steps below to your customer. Only someone with back office access to the brand can create these credentials.
</Info>

<Steps>
  <Step title="Open the Open API module">
    Sign in to the Clopos back office and go to **Add-ons → Open API**. This requires an account with the add-ons management permission.

    If the module is not listed, it has not been enabled for the brand yet — subscribe to it from the add-ons list, or contact [dev@clopos.com](mailto:dev@clopos.com).
  </Step>

  <Step title="Create the credentials">
    Click **Create credentials**. Clopos generates the **Client ID** and **Client Secret** and displays them on the page, each with a copy button.
  </Step>

  <Step title="Choose the staff user">
    Select a **Staff** member in the dropdown that appears. Every API request made with these credentials acts on behalf of that user, so their role and permissions determine what your integration is allowed to do. Pick a user whose permissions match the scope of the integration.
  </Step>

  <Step title="Enable and save">
    Turn the status toggle on and save. Authentication fails with `Client is disabled` while the module is switched off, and the form will not save until credentials have been generated.
  </Step>

  <Step title="Share the values with your integrator">
    Copy the **Client ID** and **Client Secret** and send them to your integrator, together with your **brand** identifier — the short brand slug used for your Clopos account, such as `openapitest`.
  </Step>
</Steps>

<Note>
  **Create credentials** only appears while both fields are empty. Once generated, the values stay visible on the module page so they can be copied again at any time, but they cannot be regenerated from the back office. Contact [dev@clopos.com](mailto:dev@clopos.com) if a secret needs to be rotated.
</Note>

<Warning>
  Treat the Client Secret like a password. Never commit it to source control, embed it in a mobile or browser client, or send it over an unencrypted channel.
</Warning>

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

## 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      | Generated in the customer's back office (**Add-ons → Open API**). |
| `client_secret` | string | Yes      | Paired secret, generated alongside the Client ID.                 |
| `brand`         | string | Yes      | The customer's 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](/docs/api-reference/v2/products/get-all-products)
* [Create an order](/docs/api-reference/v2/orders/create-order)
* [Manage receipts](/docs/api-reference/v2/receipts/get-receipts)
