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

ПолеТипОбязат.Описание
emailstringдаEmail отправителя
subjectstringдаТема обращения
messagestringдаТекст первого сообщения
sourceUrlstringнетURL страницы, с которой отправлена заявка
userKeystringнетID пользователя во внешнем проекте (для ответов)
userDataobjectнетДоп. данные: { "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

ПолеТипОбязат.Описание
userKeystringда*Ключ пользователя (из 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

ПолеТипОбязат.Описание
messagestringдаТекст сообщения
authorNamestringнетИмя отправителя (отобразится в админке)

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

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

ПолеТипОбязат.Описание
messagestringдаТекст ответа
authorNamestringнетИмя сотрудника (по умолчанию — «Поддержка»)

Логика

  • 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

ПолеТипОбязат.Описание
statusstringдаНовый статус: OPEN, IN_PROGRESS, RESOLVED, CLOSED
commentstringнетКомментарий к смене статуса

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

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

ПараметрТипОбязат.Описание
pageintнетНомер страницы (1-based, по умолчанию 1)
limitintнетНа странице (1-100, по умолчанию 20)
projectKeystringнетФильтр по проекту
statusstringнетФильтр по статусу
qstringнетПоиск по номеру тикета, 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'а

  1. Проверить Authorization: Bearer <webhookSecret> (секрет проекта, выданный в админ-панели; совпадает с project_settings.webhookSecret)
  2. Найти локальный тикет по ticketNumber
  3. Обновить локальный статус / добавить сообщение
  4. Отправить email/уведомление пользователю о новом ответе
  5. Вернуть 200 OK

10. Ошибки

HTTPerrorОписание
400VALIDATION_ERRORОшибка валидации полей
401UNAUTHORIZEDОтсутствует или неверный project-токен (публичные эндпоинты) либо сессионный токен/cookie
403FORBIDDENНет доступа к тикету (неверный userKey)
404NOT_FOUNDТикет не найден
500INTERNAL_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 из админки>"