HelpDesk API — Интеграционная документация
Единый API для обработки заявок поддержки со всех проектов. Встроен в wwwip.space (Express API, Drizzle ORM, PostgreSQL).
Базовый URL: https://api.wwwip.space/api/v1
1. Архитектура
┌───────────────────┐ ┌───────────────────┐
│ Внешний проект │ │ Внешний проект │
│ (profi, hoegel) │ │ (wwwip и др.) │
│ │ │ │
│ POST /api/v1/ │ │ POST /api/v1/ │
│ tickets │ │ tickets │
│ GET /api/v1/ │ │ GET /api/v1/ │
│ tickets/:id │ │ tickets/:id │
│ POST /api/v1/ │ │ POST /api/v1/ │
│ tickets/:id/msg │ │ tickets/:id/msg │
│ │ │ │
└────────┬──────────┘ └────────┬──────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ HelpDesk API │
│ встроен в ВВВИП.РФ │
│ │
│ • Приём тикетов от внешних проектов │
│ • Хранение (PostgreSQL) │
│ • Админ-панель (встроенная) │
│ • Webhook → внешним проектам │
└──────────────────────────────────────────────┘Важно: Тикеты создаются уже подтверждёнными (статус OPEN). Проект-источник сам отвечает за валидацию email, код подтверждения и авторизацию пользователя перед вызовом API.
2. Поток работы (Flow)
Пользователь Внешний проект HelpDesk API
│ │ │
│ Создаёт заявку │ │
│─────────────────────►│ │
│ │ │
│ [Проект проверяет] │ │
│ подтверждает email │ │
│ или авторизацию │ │
│ │ │
│ │ POST /api/v1/tickets │
│ │ Authorization: Bearer │
│ │ PROJECT_TOKEN │
│ │ {email, subject, │
│ │ message, │
│ │ userKey?, sourceUrl?} │
│ │──────────────────────────►│
│ │ 201 {ticketNumber, id} │
│ │◄──────────────────────────│
│ │ │
│ ← 200 {ticketNum} │ │
│◄────────────────────│ │
│ │ │
│ [Пользователь │ │
│ отвечает] │ │
│─────────────────────►│ │
│ │ POST /api/v1/tickets/:id │
│ │ /messages │
│ │ X-User-Key: xxx │
│ │ {message: "..."} │
│ │──────────────────────────►│
│ │ │
│ │ │
│ [Админ отвечает] │ │
│ │ ← Webhook message.new │
│ │◄──────────────────────────│
│ │ │
│ ← Email/уведомление│ │
│◄────────────────────│ │3. Создание тикета
Создаёт новую заявку поддержки. Вызывается внешним проектом после того, как пользователь подтвердил отправку (email-код, авторизация).
POST /api/v1/tickets Content-Type: application/json
Request body
| Поле | Тип | Обязат. | Описание |
|---|---|---|---|
email | string | да | Email отправителя |
subject | string | да | Тема обращения |
message | string | да | Текст первого сообщения |
sourceUrl | string | нет | URL страницы, с которой отправлена заявка |
userKey | string | нет | ID пользователя во внешнем проекте (для ответов) |
userData | object | нет | Доп. данные: { "name": "...", "role": "...", "avatar": "..." } |
Пример запроса
curl -X POST https://api.wwwip.space/api/v1/tickets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer PROJECT_TOKEN" \
-d '{
"email": "ivan@example.com",
"subject": "Не приходят уведомления",
"message": "Здравствуйте! Перестали приходить уведомления на почту после обновления.",
"sourceUrl": "https://profi.example.com/help",
"userKey": "user_abc123",
"userData": {
"name": "Иван Петров",
"role": "freelancer"
}
}'Response 201
{
"id": 42,
"ticketNumber": "PROFI-000042",
"status": "OPEN",
"email": "ivan@example.com",
"subject": "Не приходят уведомления",
"createdAt": "2026-07-12T12:00:00.000Z"
}Response 400
{
"error": "VALIDATION_ERROR",
"message": "email, subject and message are required",
"fields": {
"message": "required",
"subject": "required"
}
}4. Просмотр тикета
Получает тикет с историей сообщений. Для обычных пользователей требуется userKey.
GET /api/v1/tickets/:id
Query parameters
| Поле | Тип | Обязат. | Описание |
|---|---|---|---|
userKey | string | да* | Ключ пользователя (из userKey при создании) |
* Если у тикета есть userKey, то параметр обязателен и должен совпадать.
Пример запроса
curl "https://api.wwwip.space/api/v1/tickets/42?userKey=user_abc123" \ -H "Authorization: Bearer PROJECT_TOKEN"
Response 200
{
"id": 42,
"ticketNumber": "PROFI-000042",
"projectKey": "profi",
"email": "ivan@example.com",
"subject": "Не приходят уведомления",
"status": "IN_PROGRESS",
"sourceUrl": "https://profi.example.com/help",
"userKey": "user_abc123",
"userData": {
"name": "Иван Петров",
"role": "freelancer"
},
"attachmentUrl": null,
"createdAt": "2026-07-12T12:00:00.000Z",
"updatedAt": "2026-07-12T14:30:00.000Z",
"messages": [
{
"id": 1,
"authorType": "USER",
"authorName": null,
"message": "Здравствуйте! Перестали приходить уведомления...",
"attachments": [],
"createdAt": "2026-07-12T12:00:00.000Z"
},
{
"id": 2,
"authorType": "STAFF",
"authorName": "Поддержка",
"message": "Здравствуйте! Проверьте папку «Спам»...",
"attachments": [],
"createdAt": "2026-07-12T14:30:00.000Z"
}
]
}Response 403 (неверный userKey)
{
"error": "FORBIDDEN",
"message": "Access denied"
}5. Ответ пользователя
Отправляет сообщение от пользователя в существующий тикет. Требует X-User-Key.
POST /api/v1/tickets/:id/messages Content-Type: application/json X-User-Key: user_abc123
Request body
| Поле | Тип | Обязат. | Описание |
|---|---|---|---|
message | string | да | Текст сообщения |
authorName | string | нет | Имя отправителя (отобразится в админке) |
Пример запроса
curl -X POST https://api.wwwip.space/api/v1/tickets/42/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer PROJECT_TOKEN" \
-H "X-User-Key: user_abc123" \
-d '{
"message": "Спасибо, нашёл! Всё работает.",
"authorName": "Иван"
}'Response 201
{
"id": 3,
"authorType": "USER",
"message": "Спасибо, нашёл! Всё работает.",
"createdAt": "2026-07-12T15:00:00.000Z"
}6. Ответ поддержки (админ)
Отправляет сообщение от сотрудника поддержки.
POST /api/v1/admin/tickets/:id/messages Content-Type: application/json
Аутентификация: Authorization: Bearer <token> (сессионный токен из auth_tokens, получается через POST /api/auth/login).
Request body
| Поле | Тип | Обязат. | Описание |
|---|---|---|---|
message | string | да | Текст ответа |
authorName | string | нет | Имя сотрудника (по умолчанию — «Поддержка») |
Логика
authorType= STAFF- Если статус тикета
OPEN→ автоматически меняется наIN_PROGRESS - Отправляется webhook проекту (событие
message.new)
Пример запроса
curl -X POST https://api.wwwip.space/api/v1/admin/tickets/42/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"message": "Мы исправили проблему. Проверьте, пожалуйста.",
"authorName": "Мария (Поддержка)"
}'Response 201
{
"id": 4,
"authorType": "STAFF",
"authorName": "Мария (Поддержка)",
"message": "Мы исправили проблему. Проверьте, пожалуйста.",
"createdAt": "2026-07-12T16:00:00.000Z"
}7. Смена статуса (админ)
Изменяет статус тикета.
PATCH /api/v1/admin/tickets/:id/status Content-Type: application/json
Аутентификация: Authorization: Bearer <token> (сессионный токен).
Request body
| Поле | Тип | Обязат. | Описание |
|---|---|---|---|
status | string | да | Новый статус: OPEN, IN_PROGRESS, RESOLVED, CLOSED |
comment | string | нет | Комментарий к смене статуса |
Пример запроса
curl -X PATCH https://api.wwwip.space/api/v1/admin/tickets/42/status \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"status": "RESOLVED",
"comment": "Проблема исправлена, пользователь уведомлён"
}'Response 200
{
"id": 42,
"ticketNumber": "PROFI-000042",
"status": "RESOLVED",
"updatedAt": "2026-07-12T17:00:00.000Z"
}8. Список тикетов (админ)
Получает список тикетов с пагинацией и фильтрацией.
GET /api/v1/admin/tickets
Аутентификация: Authorization: Bearer <token>.
Query parameters
| Параметр | Тип | Обязат. | Описание |
|---|---|---|---|
page | int | нет | Номер страницы (1-based, по умолчанию 1) |
limit | int | нет | На странице (1-100, по умолчанию 20) |
projectKey | string | нет | Фильтр по проекту |
status | string | нет | Фильтр по статусу |
q | string | нет | Поиск по номеру тикета, email, теме |
Пример запроса
curl "https://api.wwwip.space/api/v1/admin/tickets?status=OPEN&projectKey=profi&page=1&limit=20" \ -H "Authorization: Bearer <token>"
Response 200
{
"tickets": [
{
"id": 42,
"ticketNumber": "PROFI-000042",
"projectKey": "profi",
"email": "ivan@example.com",
"subject": "Не приходят уведомления",
"status": "OPEN",
"createdAt": "2026-07-12T12:00:00.000Z",
"updatedAt": "2026-07-12T12:00:00.000Z",
"lastMessageAt": "2026-07-12T16:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 42,
"pages": 3
}
}9. Webhook → проект
Когда в HelpDesk происходит событие, он отправляет POST на webhook-эндпоинт внешнего проекта. URL и секрет webhook'а настраиваются индивидуально для каждого проекта в таблице project_settings (поля webhookUrl, webhookSecret) — через админ-панель.
Формат запроса от HelpDesk
POST <project_settings.webhookUrl> Authorization: Bearer <project_settings.webhookSecret> Content-Type: application/json
Секрет webhook'а свой у каждого проекта (не общий env HELPDESK_SECRET). Без настроенного webhookSecret вебхук не отправляется (лог Webhook ${projectKey}: webhookUrl задан, но webhookSecret не настроен).
Событие: новое сообщение от поддержки
{
"event": "message.new",
"ticketNumber": "PROFI-000042",
"projectKey": "profi",
"data": {
"message": {
"id": 4,
"authorType": "STAFF",
"authorName": "Мария (Поддержка)",
"message": "Мы ответили на ваш вопрос...",
"createdAt": "2026-07-12T16:00:00.000Z"
}
}
}Событие: смена статуса
{
"event": "ticket.updated",
"ticketNumber": "PROFI-000042",
"projectKey": "profi",
"data": {
"status": "RESOLVED",
"comment": "Проблема исправлена"
}
}Что должен делать проект при получении webhook'а
- Проверить
Authorization: Bearer <webhookSecret>(секрет проекта, выданный в админ-панели; совпадает сproject_settings.webhookSecret) - Найти локальный тикет по
ticketNumber - Обновить локальный статус / добавить сообщение
- Отправить email/уведомление пользователю о новом ответе
- Вернуть
200 OK
10. Ошибки
| HTTP | error | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR | Ошибка валидации полей |
| 401 | UNAUTHORIZED | Отсутствует или неверный project-токен (публичные эндпоинты) либо сессионный токен/cookie |
| 403 | FORBIDDEN | Нет доступа к тикету (неверный userKey) |
| 404 | NOT_FOUND | Тикет не найден |
| 500 | INTERNAL_ERROR | Внутренняя ошибка сервера |
Формат ошибки
{
"error": "VALIDATION_ERROR",
"message": "Описание ошибки",
"fields": {
"email": "invalid format"
}
}11. Безопасность
- Публичные эндпоинты (
POST /api/v1/tickets,GET /api/v1/tickets/:id,POST /api/v1/tickets/:id/messages) — требуют project API-токен:Authorization: Bearer <token>илиX-Project-Token: <token>. Без токена —401 UNAUTHORIZED. Токен хранить на backend'е проекта и не раскрывать в браузере - userKey — ключ для доступа конечного пользователя к своему тикету. Генерируется внешним проектом (UUID, cuid, или любой уникальный идентификатор)
- Admin эндпоинты (
/api/v1/admin/*) — защищены сессионными токенами (Authorization: Bearer <token>изauth_tokens,requireAuth). Только авторизованные пользователи - Webhook — защищён Bearer-токеном (
project_settings.webhookSecret— индивидуальный секрет каждого проекта) - Real-time (Socket.IO) — socket-соединения авторизуются project-токеном типа
socketизproject_tokens, комната подключения —project:<projectKey>; HTTP API socket-service (/api/emit,/api/disconnect/:userId) — заголовкиx-project-key+x-realtime-secret(секрет изproject_settings.realtimeSecret) - Номер тикета (
PROFI-000042) генерируется автоматически через автоинкрементный счётчик. Префикс =projectKey.toUpperCase()
Примеры интеграции
Node.js / TypeScript (внешний проект)
const API_BASE = 'https://api.wwwip.space/api/v1';
// Создание тикета
async function createTicket(data: {
email: string;
subject: string;
message: string;
userKey?: string;
userData?: Record<string, unknown>;
}) {
const res = await fetch(`${API_BASE}/tickets`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer PROJECT_TOKEN',
},
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`HelpDesk error: ${res.status}`);
return res.json();
}
// Получение тикета с сообщениями
async function getTicket(ticketId: number, userKey: string) {
const res = await fetch(`${API_BASE}/tickets/${ticketId}?userKey=${userKey}`, {
headers: { Authorization: 'Bearer PROJECT_TOKEN' },
});
if (!res.ok) throw new Error(`HelpDesk error: ${res.status}`);
return res.json();
}
// Ответ пользователя
async function replyToTicket(ticketId: number, userKey: string, message: string, authorName?: string) {
const res = await fetch(`${API_BASE}/tickets/${ticketId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer PROJECT_TOKEN',
'X-User-Key': userKey,
},
body: JSON.stringify({ message, authorName }),
});
if (!res.ok) throw new Error(`HelpDesk error: ${res.status}`);
return res.json();
}
// Webhook handler (проект принимает webhook от HelpDesk)
// Секрет — индивидуальный для проекта: project_settings.webhookSecret
async function handleWebhook(request: Request) {
const auth = request.headers.get('authorization');
const secret = process.env.WEBHOOK_SECRET; // секрет проекта, выданный в админ-панели
if (!auth || auth !== `Bearer ${secret}`) {
return new Response('Unauthorized', { status: 401 });
}
const payload = await request.json();
switch (payload.event) {
case 'message.new':
// Добавить сообщение в локальную БД
// Отправить email пользователю
break;
case 'ticket.updated':
// Обновить локальный статус тикета
break;
}
return new Response('OK', { status: 200 });
}Python
import requests
API_BASE = "https://api.wwwip.space/api/v1"
def create_ticket(email: str, subject: str, message: str, **kwargs):
res = requests.post(
f"{API_BASE}/tickets",
json={
"email": email,
"subject": subject,
"message": message,
**kwargs
},
headers={"Authorization": "Bearer PROJECT_TOKEN"}
)
res.raise_for_status()
return res.json()
def get_ticket(ticket_id: int, user_key: str):
res = requests.get(
f"{API_BASE}/tickets/{ticket_id}",
params={"userKey": user_key},
headers={"Authorization": "Bearer PROJECT_TOKEN"}
)
res.raise_for_status()
return res.json()
def reply_to_ticket(ticket_id: int, user_key: str, message: str, author_name: str = None):
res = requests.post(
f"{API_BASE}/tickets/{ticket_id}/messages",
headers={
"Authorization": "Bearer PROJECT_TOKEN",
"X-User-Key": user_key
},
json={"message": message, "authorName": author_name}
)
res.raise_for_status()
return res.json()Генерация номера тикета
Формат: {PREFIX}-{000001}
PREFIX=projectKey.toUpperCase()(например PROFI, HOEGEL, WWWIP)- Числовая часть — глобальный сквозной автоинкремент, 6 цифр с лидирующими нулями
- Счётчик хранится в таблице
ticket_counters
Разработка и тестирование
# Локальный запуск
npm run dev
# Создать тестовый тикет
curl -X POST http://localhost:4000/api/v1/tickets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer PROJECT_TOKEN" \
-d '{
"email": "test@example.com",
"subject": "Тестовая заявка",
"message": "Тестовое обращение"
}'
# Проверить в админке (Authorization: Bearer <сессионный токен>)
curl http://localhost:4000/api/v1/admin/tickets \
-H "Authorization: Bearer <token из админки>"