Аутентификация API — JWT, API ключи, OAuth

Как аутентифицироваться в Trigly API: JWT токены, API ключи для SDK, rate limits.

Обзор системы аутентификации

Trigly использует три механизма аутентификации в зависимости от типа интеграции:

Механизм Использование Заголовок
JWT токены Dashboard, управление через API Authorization: Bearer <token>
API ключи SDK (track, identify, batch) X-API-Key: <key>
Публичные эндпоинты Регистрация, логин, webhooks Без авторизации

Все запросы к API выполняются по HTTPS. Токены передаются в HTTP-заголовках, никогда — в URL-параметрах.


JWT токены

Структура токена

Trigly генерирует JWT-токены стандарта RFC 7519 с алгоритмом подписи HS256. Payload содержит следующие claims:

{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "org_id": "660e8400-e29b-41d4-a716-446655440001",
  "role": "owner",
  "type": "access",
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "exp": 1711209600,
  "iat": 1711207800
}
Claim Тип Описание
sub UUID Идентификатор пользователя
org_id UUID Идентификатор организации (tenant)
role string Роль: owner, admin или member
type string Тип токена: access или refresh
jti UUID Уникальный идентификатор токена (для отзыва)
exp int Время истечения (Unix timestamp)
iat int Время создания (Unix timestamp)

Получение токенов

Регистрация

При регистрации создаётся организация и пользователь-владелец. Возвращаются оба токена:

curl -X POST https://api.trigly.ru/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "organization_name": "Моя компания",
    "email": "admin@company.ru",
    "password": "SecureP@ss123!"
  }'

Ответ:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "user": {
    "id": "550e8400-...",
    "email": "admin@company.ru",
    "role": "owner",
    "organization_id": "660e8400-..."
  }
}

Логин

curl -X POST https://api.trigly.ru/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@company.ru",
    "password": "SecureP@ss123!"
  }'

Ответ идентичен регистрации — содержит access_token и refresh_token.

Использование токена

Передавайте access token в заголовке Authorization со схемой Bearer:

curl https://api.trigly.ru/api/v1/cdp/customers \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

При невалидном или истёкшем токене API вернёт 401 Unauthorized:

{
  "detail": "Invalid or expired token"
}

Обновление токенов (Refresh Flow)

Access token имеет ограниченный срок жизни (30 минут). Для обновления используйте refresh token:

curl -X POST https://api.trigly.ru/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
  }'

Ответ:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...(новый)",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...(новый)",
  "token_type": "bearer"
}

Важно: Trigly использует ротацию refresh-токенов. После каждого обновления предыдущий refresh token аннулируется. Это предотвращает повторное использование скомпрометированных токенов.

Алгоритм обновления на клиенте

Рекомендуемый поток для frontend-приложений:

  1. При получении 401 от любого запроса — попробовать обновить токен
  2. Если обновление успешно — повторить исходный запрос с новым access token
  3. Если обновление неуспешно (refresh token тоже истёк) — перенаправить на логин
// Пример interceptor для axios/fetch
async function apiRequest(url, options) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${getAccessToken()}`
    }
  });

  if (response.status === 401) {
    const refreshResult = await fetch('/api/v1/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: getRefreshToken() })
    });

    if (refreshResult.ok) {
      const tokens = await refreshResult.json();
      saveTokens(tokens);
      // Повторяем исходный запрос
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${tokens.access_token}`
        }
      });
    } else {
      redirectToLogin();
    }
  }

  return response;
}

Выход из системы (Logout)

Выход на текущем устройстве

Отзывает текущий refresh token:

curl -X POST https://api.trigly.ru/api/v1/auth/logout \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'

Выход на всех устройствах

Отзывает все refresh-токены пользователя:

curl -X POST https://api.trigly.ru/api/v1/auth/logout-all \
  -H "Authorization: Bearer $TOKEN"

Ролевая модель

Роль Права
owner Полный доступ. Управление пользователями, биллинг, удаление организации
admin Управление кампаниями, контактами, каналами. Приглашение пользователей
member Просмотр данных, создание кампаний. Без управления пользователями

Проверка роли на серверной стороне:

from app.common.dependencies import require_role

# Только owner и admin
@router.post("/invite")
async def invite_user(
    data: InviteRequest,
    user = Depends(require_role(["owner", "admin"])),
    db: AsyncSession = Depends(get_db)
):
    ...

Приглашение пользователей

Только owner и admin могут приглашать новых пользователей в организацию:

curl -X POST https://api.trigly.ru/api/v1/auth/invite \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "manager@company.ru",
    "role": "member"
  }'

API ключи

API ключи предназначены для серверной и клиентской интеграции через SDK. Они не привязаны к конкретному пользователю, а к организации.

Создание API ключа

curl -X POST https://api.trigly.ru/api/v1/cdp/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Продакшн сайт"}'

Ответ:

{
  "id": "990e8400-...",
  "name": "Продакшн сайт",
  "api_key": "tgl_live_abc123def456ghi789...",
  "created_at": "2026-03-20T10:00:00Z"
}

Важно: Значение api_key показывается только при создании. Сохраните его в безопасном месте.

Использование API ключа

Передавайте ключ в заголовке X-API-Key:

curl -X POST https://api.trigly.ru/api/v1/sdk/track \
  -H "X-API-Key: tgl_live_abc123def456..." \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "customer-uuid",
    "event_name": "page_view",
    "properties": {"page": "/products/123"}
  }'

Доступные SDK-эндпоинты

API ключи обеспечивают доступ только к SDK-эндпоинтам:

Эндпоинт Метод Назначение
/api/v1/sdk/track POST Отслеживание события
/api/v1/sdk/batch POST Пакетная отправка событий
/api/v1/sdk/identify POST Идентификация пользователя
/api/v1/sdk/push/vapid-key GET Получение VAPID ключа
/api/v1/sdk/push/subscribe POST Подписка на push
/api/v1/sdk/push/unsubscribe POST Отписка от push
/api/v1/sdk/channels/widgets GET Виджеты подписки
/api/v1/sdk/channels/subscribe POST Подписка на каналы

Список и отзыв ключей

# Список всех API ключей
curl https://api.trigly.ru/api/v1/cdp/api-keys \
  -H "Authorization: Bearer $TOKEN"

# Отзыв ключа
curl -X DELETE https://api.trigly.ru/api/v1/cdp/api-keys/{key_id} \
  -H "Authorization: Bearer $TOKEN"

Публичные эндпоинты

Следующие пути не требуют аутентификации:

Путь Описание
POST /api/v1/auth/register Регистрация
POST /api/v1/auth/login Логин
POST /api/v1/auth/refresh Обновление токена
/api/v1/sdk/* SDK-эндпоинты (авторизация через API-ключ)
/hooks/* Webhook-эндпоинты провайдеров
GET /api/v1/campaigns/track/pixel Пиксель открытия
GET /api/v1/campaigns/track/click Отслеживание кликов
GET /api/v1/campaigns/track/unsubscribe Отписка

Rate Limits

Trigly применяет ограничения на количество запросов для защиты от злоупотреблений. Rate limiting реализован на базе Redis с использованием скользящего окна.

Лимиты по группам

Группа эндпоинтов Лимит Окно
/api/v1/auth/* 200 запросов 60 секунд
/api/v1/sdk/* 1000 запросов 60 секунд
Все остальные 500 запросов 60 секунд

Заголовки ответа

При каждом запросе API возвращает информацию о текущем состоянии лимита:

X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1711209660

Превышение лимита

При превышении API вернёт 429 Too Many Requests:

{
  "detail": "Rate limit exceeded. Try again in 23 seconds."
}

Рекомендации:

  • Используйте пакетные эндпоинты (/sdk/batch) вместо одиночных запросов
  • Реализуйте exponential backoff при получении 429
  • Кэшируйте ответы на стороне клиента где возможно
  • Для массовых операций используйте bulk-эндпоинты (/api/v1/cdp/contacts/bulk/*)

Мультитенантность

Trigly — мультитенантная платформа. Каждый запрос автоматически ограничен организацией текущего пользователя. Идентификатор организации извлекается из JWT-токена (claim org_id) и применяется как фильтр ко всем запросам к базе данных.

Это означает:

  • Пользователь организации A не может видеть или изменять данные организации B
  • API ключи привязаны к организации и дают доступ только к её данным
  • Все запросы к БД содержат WHERE organization_id = :org_id
  • Суперадмин-эндпоинтов нет — изоляция абсолютная

Безопасность

  • Пароли хэшируются через bcrypt (cost factor 12)
  • JWT подписываются HMAC-SHA256 с секретным ключом
  • Refresh-токены хранятся в PostgreSQL с привязкой к jti
  • При логауте refresh-токен помечается как отозванный (is_revoked = true)
  • API ключи хранятся в хэшированном виде

Примеры на разных языках

Python (httpx)

import httpx

client = httpx.AsyncClient(
    base_url="https://api.trigly.ru",
    headers={"Authorization": f"Bearer {access_token}"}
)

# Получение контактов
response = await client.get("/api/v1/cdp/customers", params={"page": 1, "per_page": 20})
customers = response.json()

# Обработка 401
if response.status_code == 401:
    refresh_response = await client.post("/api/v1/auth/refresh", json={
        "refresh_token": refresh_token
    })
    new_tokens = refresh_response.json()
    client.headers["Authorization"] = f"Bearer {new_tokens['access_token']}"

JavaScript (fetch)

const API_BASE = 'https://api.trigly.ru';

async function triglyAPI(path, options = {}) {
  const response = await fetch(`${API_BASE}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
      ...options.headers,
    },
  });

  if (response.status === 401) {
    // Обновление токена
    const refreshRes = await fetch(`${API_BASE}/api/v1/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        refresh_token: localStorage.getItem('refresh_token')
      }),
    });

    if (refreshRes.ok) {
      const tokens = await refreshRes.json();
      localStorage.setItem('access_token', tokens.access_token);
      localStorage.setItem('refresh_token', tokens.refresh_token);
      return triglyAPI(path, options); // Повтор
    }

    window.location.href = '/login';
  }

  return response.json();
}

PHP (cURL)

<?php
function triglyRequest($method, $path, $data = null) {
    $ch = curl_init("https://api.trigly.ru" . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Authorization: Bearer " . $_SESSION['access_token'],
        "Content-Type: application/json"
    ]);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);

    if ($data) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 401) {
        // Обновить токен и повторить
        refreshTokens();
        return triglyRequest($method, $path, $data);
    }

    return json_decode($response, true);
}
?>

Не нашли ответ?

Swagger UI с интерактивной документацией и поддержка в Telegram.