Аутентификация 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-приложений:
- При получении
401от любого запроса — попробовать обновить токен - Если обновление успешно — повторить исходный запрос с новым access token
- Если обновление неуспешно (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.