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

# Работа с API

> Базовый URL, аутентификация, формат ответа и ошибок, rate-limit и пагинация

Exode SaaS API — это REST API поверх HTTPS. Все эндпоинты находятся под префиксом `saas/v2/`.

## Базовый URL

```text theme={null}
https://api.exode.biz
```

Полный адрес метода: `https://api.exode.biz/saas/v2/<module>/<method>` — например `https://api.exode.biz/saas/v2/user/create`.

## Аутентификация

Запросы выполняются от имени **сервисного пользователя** (API-клиента) — это сотрудник школы с настроенным
набором прав и собственным API-токеном.

* Аутентификация — через заголовок `Authorization: Bearer <TOKEN>`.
* Токен бессрочный, его можно отозвать в любой момент.
* Токен должен принадлежать пользователю с признаком **API-клиента** — иначе SaaS-эндпоинты вернут ошибку
  доступа, даже если остальные права на месте.

<Card title="Создание сервисного пользователя" icon="telegram" href="https://t.me/exode_support_biz">
  Напишите в поддержку — мы создадим сервисного пользователя с нужными правами и выдадим API-токен.
</Card>

<Warning>
  Никогда не храните токен в коде или репозитории. Используйте переменные окружения. Токен даёт доступ к
  данным школы — не передавайте его третьим лицам.
</Warning>

## Обязательные заголовки

## Заголовки запроса

<ParamField header="Authorization" type="string" required>
  API токен сервисного пользователя в формате Bearer. Получите токен в панели администратора школы. Формат: `Bearer YOUR_TOKEN`.
</ParamField>

<ParamField header="Seller-Id" type="string" required>
  Уникальный идентификатор продавца в системе. Используется для разграничения доступа между разными продавцами.
</ParamField>

<ParamField header="School-Id" type="string" required>
  Уникальный идентификатор школы в системе. Определяет контекст выполнения операции.
</ParamField>

<Warning>
  Отсутствие или невалидность `Authorization` приводит к ошибке `401`. `Seller-Id` и `School-Id` определяют
  контекст продавца и школы; без них защищённые методы вернут `401`.
</Warning>

## Формат ответа

Любой успешный ответ обёрнут в единую структуру:

<ResponseField name="success" type="boolean" required>
  `true` для успешных ответов (HTTP-коды `200`–`206`).
</ResponseField>

<ResponseField name="code" type="integer" required>
  HTTP-код ответа (`200`, `201`, и т.д.).
</ResponseField>

<ResponseField name="payload" type="any" required>
  Полезная нагрузка метода. Структура описана на странице конкретного метода и строго соответствует
  схеме ответа (лишние поля не возвращаются).
</ResponseField>

```json Успешный ответ theme={null}
{
  "success": true,
  "code": 200,
  "payload": { /* method data */ }
}
```

## Формат ошибок

Ошибки возвращаются с тем же конвертом плюс полями `cause`, `message`, `error` и опциональным `data`:

<ResponseField name="success" type="boolean" required>
  Всегда `false` для ошибок.
</ResponseField>

<ResponseField name="code" type="integer" required>
  HTTP-код ошибки: `400`, `401`, `403`, `404`, `429`, `500`.
</ResponseField>

<ResponseField name="cause" type="string" required>
  Машиночитаемый код причины. По нему удобно строить обработку. Примеры: `validation`, `Unauthorized`,
  `Forbidden`, `EmailIsBusy`, `Rate`.
</ResponseField>

<ResponseField name="message" type="string" required>
  Человекочитаемое сообщение об ошибке.
</ResponseField>

<ResponseField name="error" type="string" required>
  Техническое описание (по умолчанию совпадает с `message`).
</ResponseField>

<ResponseField name="data" type="object">
  Дополнительные данные (например, `retryAfter` для `429`). Опционально.
</ResponseField>

```json Пример ошибки theme={null}
{
  "success": false,
  "code": 400,
  "cause": "EmailIsBusy",
  "message": "Email is busy",
  "error": "Email is busy"
}
```

### Частые коды причин (`cause`)

| HTTP | `cause`        | Когда возникает                                                                          |
| ---- | -------------- | ---------------------------------------------------------------------------------------- |
| 400  | `validation`   | Тело/параметры не прошли валидацию (тип, длина, формат, enum)                            |
| 401  | `Unauthorized` | Отсутствует/невалиден токен (`Invalid token credentials`)                                |
| 401  | `Blocked`      | Пользователь токена неактивен или забанен                                                |
| 401  | `Forbidden`    | Нет доступа к ресурсу продавца/школы (недостаточно прав или ресурс не принадлежит школе) |
| 403  | `Forbidden`    | Недостаточно прав (RBAC): `Доступ к ресурсу ограничен`                                   |
| 429  | `Rate`         | Превышен лимит запросов (см. ниже)                                                       |

<Tip>
  Конкретные `cause`-коды для каждого метода перечислены на его странице (раздел ответов «Error»). Например,
  при создании пользователя возможны `UserAlreadyExist`, `EmailIsBusy`, `PhoneIsBusy`, `TgIdIsBusy`.
</Tip>

## Права доступа (RBAC)

Каждый метод требует у токена определённых прав. Если у метода указано несколько прав — достаточно **любого
одного** из них (семантика OR). Примеры прав, используемых в SaaS API:

| Право                  | Назначение                                                                        |
| ---------------------- | --------------------------------------------------------------------------------- |
| `SchoolManageUsers`    | Управление пользователями школы (создание, обновление, удаление, группы, доступы) |
| `SchoolManageSettings` | Настройки школы, в т.ч. вебхуки                                                   |
| `CourseCurator`        | Кураторская работа: проверка практик, прогресс студентов                          |
| `CourseStudentManage`  | Управление студентами курса (зачисление, отчисление)                              |
| `SellerSales`          | Доступ к продажам: счета, платежи, финансовые отчёты                              |
| `FormManage`           | Управление формами, макетами и значениями полей                                   |

## Rate-limit

Часть методов ограничена по частоте вызовов. При превышении возвращается **HTTP `429`** с `cause: "Rate"`:

```json Превышение лимита theme={null}
{
  "success": false,
  "code": 429,
  "cause": "Rate",
  "message": "Rate limit exceeded",
  "data": { "retryAfter": "2025-01-18T12:45:10.000Z" }
}
```

* Лимит считается по сервисному пользователю (токену).
* Поле `data.retryAfter` подсказывает, когда можно повторить запрос.
* Лимиты указаны на страницах методов. Например, генерация выгрузок — **100 запросов в час**.

<Info>
  Реализуйте retry с учётом `retryAfter` и экспоненциальной задержкой для временных ошибок (`429`, `5xx`).
</Info>

## Пагинация

Списочные методы (`.../list/raw`, прогресс по курсу и т.п.) принимают параметры пагинации в query:

<ParamField query="take" type="integer" required={false}>
  Размер страницы. От `1` до `1000`. По умолчанию `100`.
</ParamField>

<ParamField query="page" type="integer" required={false}>
  Номер страницы (начиная с `1`). Альтернатива `skip`.
</ParamField>

<ParamField query="skip" type="integer" required={false}>
  Смещение (число пропускаемых записей). Игнорируется, если задан `page`.
</ParamField>

Ответ списочного метода — единый конверт со страницей:

```json Структура страницы theme={null}
{
  "success": true,
  "code": 200,
  "payload": {
    "items": [ /* items */ ],
    "page": 1,
    "count": 245,
    "pages": 3,
    "isFirst": true,
    "isLast": false,
    "next": { "skip": 100, "take": 100, "page": 2 },
    "prev": { "skip": 0, "take": 100, "page": 1 }
  }
}
```

<Info>
  Параметры-массивы передаются повторением ключа: `userIds=1&userIds=2&userIds=3`. Диапазоны передаются
  как вложенные поля, например `createdAtDateRange[from]` и `createdAtDateRange[to]`.
</Info>

## Пример запроса

<RequestExample>
  ```bash cURL theme={null}
  curl --location 'https://api.exode.biz/saas/v2/user/find?extId=crm_12345' \
    --header 'Authorization: Bearer YOUR_TOKEN' \
    --header 'Seller-Id: {{ sellerId }}' \
    --header 'School-Id: {{ schoolId }}'
  ```

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

  const client = axios.create({
    baseURL: 'https://api.exode.biz/saas/v2',
    headers: {
      Authorization: `Bearer ${process.env.EXODE_TOKEN}`,
      'Seller-Id': process.env.SELLER_ID,
      'School-Id': process.env.SCHOOL_ID,
    },
  });

  const { data } = await client.get('/user/find', { params: { extId: 'crm_12345' } });
  console.log(data.payload.user);
  ```

  ```php PHP theme={null}
  <?php
  $url = 'https://api.exode.biz/saas/v2/user/find?extId=crm_12345';
  $headers = [
    'Authorization: Bearer ' . $_ENV['EXODE_TOKEN'],
    'Seller-Id: ' . $_ENV['SELLER_ID'],
    'School-Id: ' . $_ENV['SCHOOL_ID'],
  ];

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  echo curl_exec($ch);
  curl_close($ch);
  ```

  ```python Python theme={null}
  import os, requests

  session = requests.Session()
  session.headers.update({
    'Authorization': f"Bearer {os.environ['EXODE_TOKEN']}",
    'Seller-Id': os.environ['SELLER_ID'],
    'School-Id': os.environ['SCHOOL_ID'],
  })

  r = session.get('https://api.exode.biz/saas/v2/user/find', params={'extId': 'crm_12345'})
  print(r.json()['payload']['user'])
  ```

  ```bsl 1С theme={null}
  Соединение = Новый HTTPСоединение("api.exode.biz", 443, , , , 30, Новый OpenSSLSecureConnection);

  Запрос = Новый HTTPЗапрос("/saas/v2/user/find?extId=crm_12345");
  Запрос.Заголовки.Вставить("Seller-Id", "{{ sellerId }}");
  Запрос.Заголовки.Вставить("School-Id", "{{ schoolId }}");
  Запрос.Заголовки.Вставить("Authorization", "Bearer YOUR_TOKEN");

  Ответ = Соединение.ВызватьHTTPМетод("GET", Запрос);

  Если Ответ.КодСостояния = 200 Тогда
      ЧтениеJSON = Новый ЧтениеJSON;
      ЧтениеJSON.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
      Результат = ПрочитатьJSON(ЧтениеJSON);
      Сообщить("Пользователь найден");
  Иначе
      Сообщить("Ошибка: HTTP " + Ответ.КодСостояния);
      Сообщить(Ответ.ПолучитьТелоКакСтроку());
  КонецЕсли;
  ```
</RequestExample>

<Tip>
  Всегда проверяйте `success`/`code` и обрабатывайте `cause` ошибки — это ускорит диагностику интеграции.
</Tip>
