> ## 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 Client

> Серверный клиент ExodeAPI для работы с SaaS API

`ExodeAPI` — типизированный HTTP-клиент, покрывающий эндпоинты `/saas/v2/*`. Предназначен для сервера (Node.js),
инкапсулирует аутентификацию, сериализацию query/тела, обработку ошибок и (опционально) валидацию ответов по
zod-схемам.

<Info>
  Полная REST-спецификация (request/response, коды ошибок) — в разделе [Exode API](/ru/exode-api/setup).
  SDK — типизированная обёртка над тем же контрактом; типы выведены из тех же серверных zod-схем.
</Info>

## Инициализация

```ts theme={null}
import { ExodeAPI } from '@exode-team/sdk/api'

const exodeApi = new ExodeAPI({
  sellerId: 1,
  schoolId: 1,
  token: process.env.EXODE_TOKEN!,
  baseUrl: 'https://api.exode.biz/saas/v2', // default
  timeout: 30_000,                           // default 30s
})
```

### Параметры конфигурации

<ResponseField name="sellerId" type="number" required>
  Идентификатор продавца. Передаётся в заголовке `Seller-Id`.
</ResponseField>

<ResponseField name="schoolId" type="number" required>
  Идентификатор школы. Передаётся в заголовке `School-Id`.
</ResponseField>

<ResponseField name="token" type="string" required>
  API-токен сервисного пользователя. Передаётся в `Authorization: Bearer <token>`.
</ResponseField>

<ResponseField name="baseUrl" type="string">
  Базовый URL API. По умолчанию `https://api.exode.biz/saas/v2`.
</ResponseField>

<ResponseField name="timeout" type="number">
  Таймаут запроса в миллисекундах. По умолчанию `30000`. При превышении — `ExodeAPIError` с `cause: "Timeout"` (code 408).
</ResponseField>

<Warning>
  Храните токен в переменных окружения. Никогда не коммитьте токены и не передавайте на клиент — `ExodeAPI`
  работает только на сервере.
</Warning>

## Доступные ресурсы

Клиент предоставляет namespace `school` с семью ресурсами:

<CardGroup cols={2}>
  <Card title="school.user" icon="user">CRUD пользователей, состояние (state), удаление, токены авторизации.</Card>
  <Card title="school.group" icon="users">Список групп, массовое добавление/удаление участников.</Card>
  <Card title="school.course" icon="graduation-cap">Список курсов и прогресс участников по курсу.</Card>
  <Card title="school.productAccess" icon="box">Список доступов к продуктам.</Card>
  <Card title="school.invoice" icon="receipt">Список счетов.</Card>
  <Card title="school.form" icon="list-check">Макеты форм и значения кастомных полей.</Card>
  <Card title="school.queryExport" icon="file-export">Генерация и опрос результата выгрузок.</Card>
</CardGroup>

## Пользователи (`school.user`)

```ts theme={null}
import { UserStateKey } from '@exode-team/sdk/api'

// Create — returns the user object (or null)
const user = await exodeApi.school.user.create({
  email: 'student@example.com',
  profile: { firstName: 'John', role: 'Student' },
})

// Update — returns the user
await exodeApi.school.user.update(user.id, { profile: { lastName: 'Smith' } })

// Upsert (create or update by email/phone/tgId/extId)
const { user: u, isCreated } = await exodeApi.school.user.upsert({
  email: 'student@example.com',
  profile: { firstName: 'John' },
})

// Find — exactly one of login | tgId | extId
const found = await exodeApi.school.user.find({ login: 'student@example.com' })

// Delete (≤250 at a time) — { deleted, skipped }
const { deleted, skipped } = await exodeApi.school.user.deleteMany([1, 2, 3], 'Access revocation')

// State — key from the UserStateKey enum
await exodeApi.school.user.setState(user.id, UserStateKey.OnBoardingProgress, { step: 2 }) // → boolean
const value = await exodeApi.school.user.getState(user.id, UserStateKey.OnBoardingProgress) // → unknown

// Auto-auth token — { session, isCreated }
const { session } = await exodeApi.school.user.createAuthToken({ userId: user.id })
// session.token → put into ?___uat=<token> for auto-login
```

<Tip>
  Подробнее про автологин через `___uat` — в разделе
  [Интеграция с Telegram Mini App](/ru/exode-api/school/iframe/tg-mini-app).
</Tip>

## Группы (`school.group`)

```ts theme={null}
// List groups (filter + pagination)
const { items } = await exodeApi.school.group.list({ courseIds: [10], take: 50 })

// Add members (≤250) — { exist, created }
await exodeApi.school.group.addMembers(groupId, [1, 2, 3])

// Remove members (≤250) — { affected }
await exodeApi.school.group.removeMembers(groupId, [1])
```

## Курсы (`school.course`)

```ts theme={null}
// List courses
const { items } = await exodeApi.school.course.list({ types: ['VideoCourse'], take: 20 })

// Course members' progress (pagination)
const progress = await exodeApi.school.course.progresses(courseId, { take: 100 })
```

## Доступы к продуктам (`school.productAccess`)

```ts theme={null}
const { items } = await exodeApi.school.productAccess.list({ active: true, take: 50 })
```

## Счета (`school.invoice`)

```ts theme={null}
const { items } = await exodeApi.school.invoice.list({ types: ['Regular'], take: 50 })
```

## Формы (`school.form`)

```ts theme={null}
// Form layouts
const layout = await exodeApi.school.form.layoutCreate({ mode: 'Custom', name: 'Survey', internalName: 'survey' })
await exodeApi.school.form.layoutUpdate(layout.id, { status: 'Published' })
await exodeApi.school.form.layoutDelete(layout.id) // → { affected }

// Custom field values
const { items } = await exodeApi.school.form.customFieldValueGet({ userIds: [27], take: 50 })

// Set values by slug (type is inferred automatically)
await exodeApi.school.form.customFieldValueSetBySlug({
  userId: 27,
  layoutId: layout.id,
  values: [{ slug: 'city', value: 'Tashkent' }, { slug: 'age', value: 25 }],
})

// Set values by fieldId (typed)
await exodeApi.school.form.customFieldValueSet({
  userId: 27,
  layoutId: layout.id,
  values: [{ fieldId: 10, text: 'Tashkent' }],
})
```

## Выгрузки (`school.queryExport`)

Выгрузки работают асинхронно: генерация запускает workflow, результат опрашивается по UUID.

```ts theme={null}
import { QueryExportType, QueryExportFormat } from '@exode-team/sdk/api'

// 1. Start — returns { uuid, status, ... }
const { uuid } = await exodeApi.school.queryExport.generate({
  type: QueryExportType.GroupMemberFindMany,
  format: QueryExportFormat.Csv,
  variables: { filter: { groupIds: [42] } },
})

// 2. Poll for result (repeat until status becomes Completed/Failed)
const result = await exodeApi.school.queryExport.getResult(uuid)

if (result?.status === 'Completed') {
  console.log('File:', result.result?.fileUrl)
}
```

### Статусы выполнения

| Статус       | Значение                                                           |
| ------------ | ------------------------------------------------------------------ |
| `Waiting`    | Задача в очереди                                                   |
| `Processing` | Выполняется                                                        |
| `Completed`  | Готово, `result` содержит файл (`fileUrl`, `fileName`, `fileSize`) |
| `Failed`     | Ошибка                                                             |
| `Canceled`   | Отменено                                                           |

<Info>
  `generate` ограничен лимитом **100 запросов в час**. Типы отчётов и переменные — в разделе
  [Отчёты и выгрузки](/ru/exode-api/school/query-export/generate).
</Info>

## Типизация ответов

Типы всех ответов выведены из тех же серверных zod-схем (через `z.infer`) и доступны на импорт
(`User`, `Profile`, `Session`, `Group`, `GroupMember`, `CourseProgress`, `FormLayout`, `FormFieldValue`,
а также `*Output`-типы). Рантайм-валидации на стороне клиента **нет**: контракты уже проверяются на бэкенде
(`@ZodResponse`), поэтому zod не попадает в рантайм-бандл — ответ возвращается как есть, строго типизированным.

```ts theme={null}
import type { User, ListGroupOutput } from '@exode-team/sdk/api'
```

## Обработка ошибок

Все ошибки оборачиваются в `ExodeAPIError`:

```ts theme={null}
import { ExodeAPI, ExodeAPIError } from '@exode-team/sdk/api'

try {
  await exodeApi.school.user.create({ email: 'busy@example.com' })
} catch (error) {
  if (error instanceof ExodeAPIError) {
    console.error(error.code)        // 400, 401, 403, 408, 429, 0 (сеть)...
    console.error(error.errorCause)  // 'EmailIsBusy', 'Unauthorized', 'Timeout', 'NetworkError'...
    console.error(error.details)     // technical description (optional)
  }
}
```

| `errorCause`                                                          | Когда                         |
| --------------------------------------------------------------------- | ----------------------------- |
| доменные коды (`EmailIsBusy`, `Unauthorized`, `Forbidden`, `Rate`, …) | ответ API с ошибкой           |
| `Timeout`                                                             | превышен `timeout` (code 408) |
| `NetworkError`                                                        | сетевая ошибка (code 0)       |
| `ParseError`                                                          | ответ не разобрался как JSON  |

<Info>
  Полный список доменных `cause`-кодов — в разделе [Работа с API](/ru/exode-api/setup#формат-ошибок).
</Info>
