H Hub

API Hub

REST API поверх бэклога: читать отчёты, собранные виджетом, создавать задачи, править их, двигать между колонками. Всё отвечает JSON, всё авторизуется одним bearer-токеном.

Базовый адрес: https://hub.doctorweedy.com/api/v1

Аутентификация

Каждый запрос несёт ключ bearer-токеном. Ключи выдаются в кабинете на странице «Ключи API», показываются один раз и хранятся хешем; у каждого свои права, его можно ограничить проектами, задать срок и отозвать в любой момент.

Authorization: Bearer hub_ak_…

Только с сервера. Ключ пространства несёт все выданные ему права, поэтому его место в окружении бэкенда, а не в разметке страницы и не в собранном скрипте. Кросс-доменные вызовы этого API из браузера намеренно запрещены — на них отвечает только приём отчётов.

Три ключа, три задачи

Их обычно и путают: ни один ключ со вкладки «Общее» в проекте не умеет читать бэклог.

Ключ Где взять Что даёт
pk_… Проект → Общее Позволяет виджету слать отчёты. Публичный — лежит в исходнике страницы.
sk_… Проект → Общее Подписывает личность отправителя на вашем сервере. Больше ничего.
hub_ak_… Кабинет → Ключи API Читает и правит бэклог. Именно о нём эта страница.

Эндпоинты

GET /me Чей это ключ: пространство, права, видимые проекты. Начните отсюда.
GET /projects Проекты, доступные ключу, со счётчиком задач.
GET /context/{project} Статусы, типы, метки, области и люди одним ответом.
GET /tasks Бэклог. Фильтры: status, type, label, source, since, limit.
GET /tasks/{id} Одна задача со скриншотами и диагностикой.
POST /tasks Создать задачу. Обязательны project и title.
PATCH /tasks/{id} Изменить название, описание, тип, приоритет, метку, область.
POST /tasks/{id}/move Сменить статус — и, если нужно, порядок внутри него.
DELETE /tasks/{id} Удалить задачу.
GET /people Отправители, самые активные сверху.

Ключ, ограниченный проектами, видит только их; ключу с одним чтением любая запись будет отклонена.

Фильтры списка

Параметр Значения Что делает
project слаг проекта Только этот проект.
status backlog · in_progress · review · done Только эта колонка.
type bug · idea · question · task Только задачи этого типа.
label строка Только задачи с этой меткой.
source widget · manual · import Откуда пришла задача.
since дата или ISO 8601 Созданы не раньше этого момента.
limit 1–500, по умолчанию 200 Сколько задач вернуть. Сводка всё равно считает все.

Что отвечает список

GET https://hub.doctorweedy.com/api/v1/tasks?project=your-project

{
  "tasks": [
    {
      "id": 1,
      "project": "your-project",
      "project_name": "Your Project",
      "project_url": "https://example.com",
      "title": "Checkout is broken",
      "description": "Pressed pay, nothing happened",
      "status": "backlog",
      "type": "bug",
      "priority": "normal",
      "label": "bug",
      "area": null,
      "assignee": null,
      "reporter_person": { "name": "Marina", "email": "marina@example.com", "verified": true },
      "identity_verified": true,
      "locale": "en",
      "files": [],
      "source": "widget",
      "reporter": { "url": "https://example.com/checkout", "viewport": "1512x731", "user_agent": "…" },
      "diagnostics": { "console": [ … ], "network": [ … ], "steps": [ … ] },
      "screenshots": [ "https://…/shot.png" ],
      "created_at": "2026-07-24T09:12:44+00:00",
      "updated_at": "2026-07-24T09:12:44+00:00"
    }
  ],
  "meta": { "total": 42, "returned": 20, "limit": 20, "by_status": { "backlog": 30, "done": 12 } }
}

Сводка считается по тем же фильтрам, но до ограничения по количеству: тот, кто рисует счётчики, узнаёт настоящее число задач без второго запроса.

Поля задачи

Поле Тип Доступ
id число чтение
project, project_name, project_url строка чтение; в запись адресует project
title строка чтение, запись
description строка чтение, запись
status backlog · in_progress · review · done чтение; запись через /move
type bug · idea · question · task чтение, запись
priority low · normal · high · urgent чтение, запись
label, area строка чтение, запись
assignee строка чтение
reporter_person объект чтение — кто сообщил, если известно
identity_verified да/нет чтение — подписана ли личность секретным ключом
locale строка чтение — язык страницы, с которой пришёл отчёт
files массив строк чтение, запись
screenshots массив ссылок чтение
diagnostics объект чтение — консоль, сеть, шаги
reporter объект чтение — адрес, размер окна, браузер
source widget · manual · import чтение
created_at, updated_at ISO 8601 чтение

Статус — единственное поле, которое PATCH не трогает: перенос задачи решает ещё и её место в колонке, поэтому у него свой запрос.

Коды ответов

200 · 201 Готово. Запись отвечает той задачей, которую записала.
401 Ключа нет, ключ неизвестен, отозван или просрочен.
403 Ключ рабочий, но права не хватает: ключ с одним чтением пытается писать или лезет в невыданный проект.
404 Такой задачи нет — или она чужого пространства. Одно от другого намеренно не отличить.
422 Тело запроса не прошло проверку. В ответе перечислены поля.

Проверить из терминала

# 1. Is the key alive and what does it see?
curl -s -H "Authorization: Bearer $HUB_API_KEY" https://hub.doctorweedy.com/api/v1/me

# 2. The backlog of one project
curl -s -H "Authorization: Bearer $HUB_API_KEY" "https://hub.doctorweedy.com/api/v1/tasks?project=your-project&status=backlog"

# 3. Create a task
curl -s -X POST -H "Authorization: Bearer $HUB_API_KEY" -H "Content-Type: application/json" \
  -d '{"project":"your-project","title":"Checkout is broken","description":"Steps…","type":"bug"}' \
  https://hub.doctorweedy.com/api/v1/tasks

# 4. Rename it, then move it to done
curl -s -X PATCH -H "Authorization: Bearer $HUB_API_KEY" -H "Content-Type: application/json" \
  -d '{"title":"Checkout fails on card payment"}' https://hub.doctorweedy.com/api/v1/tasks/123

curl -s -X POST -H "Authorization: Bearer $HUB_API_KEY" -H "Content-Type: application/json" \
  -d '{"status":"done"}' https://hub.doctorweedy.com/api/v1/tasks/123/move

Клиент для вашего проекта

Пример на Laravel. Подойдёт любой HTTP-клиент — форма запросов та же.

// config/services.php
'hub' => [
    'url' => env('HUB_URL'),
    'key' => env('HUB_API_KEY'),
    'project' => env('HUB_PROJECT'),
],

// app/Support/Hub.php
class Hub
{
    protected function http(): PendingRequest
    {
        return Http::withToken(config('services.hub.key'))
            ->baseUrl(config('services.hub.url') . '/api/v1')
            ->acceptJson()
            ->timeout(10);
    }

    /** @return array<int,array<string,mixed>> */
    public function tasks(?string $status = null): array
    {
        return $this->http()->get('/tasks', array_filter([
            'project' => config('services.hub.project'),
            'status' => $status,
        ]))->throw()->json('tasks');
    }

    public function create(string $title, string $description = ''): array
    {
        return $this->http()->post('/tasks', [
            'project' => config('services.hub.project'),
            'title' => $title,
            'description' => $description,
        ])->throw()->json('task');
    }

    public function move(int $id, string $status): void
    {
        $this->http()->post("/tasks/{$id}/move", ['status' => $status])->throw();
    }
}

То же на JavaScript:

const hub = async (path, init = {}) => {
  const response = await fetch(`${process.env.HUB_URL}/api/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.HUB_API_KEY}`,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });

  if (!response.ok) throw new Error(`Hub ${response.status}: ${await response.text()}`);

  return response.json();
};

const { tasks, meta } = await hub('/tasks?project=your-project&status=backlog');
await hub('/tasks', { method: 'POST', body: JSON.stringify({ project: 'your-project', title: 'Filed by an agent' }) });

Дальше выводите где угодно

{{-- resources/views/backlog.blade.php --}}
@foreach (app(Hub::class)->tasks('backlog') as $task)
    <article>
        <h3>{{ $task['title'] }}</h3>
        <p>{{ $task['description'] }}</p>
        <small>{{ $task['project_name'] }} · {{ $task['status'] }} · {{ $task['type'] }}</small>
        @foreach ($task['screenshots'] as $url)
            <img src="{{ $url }}" alt="">
        @endforeach
    </article>
@endforeach

Ссылки на скриншоты абсолютные и публичные — их можно ставить прямо в тег img.

Вебхуки: отчёт приходит к вам сам

API выше работает на запрос: вы спрашиваете, Hub отвечает. Вебхук — обратное направление: добавьте его на вкладке «Уведомления» проекта, и каждый новый отчёт будет приходить на ваш адрес сам.

Что приходит: POST с двумя заголовками и телом в JSON.

POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Hub-Event: report.created
X-Hub-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015…

{
  "event": "report.created",
  "sent_at": "2026-07-28T10:15:00+00:00",
  "task": { "id": 48, "project": "langeater", "title": "…", "screenshots": ["…"] }
}

У объекта задачи ровно те поля, что перечислены выше, — та же форма, что отдаёт GET /tasks.

Проверьте подпись, прежде чем доверять телу запроса

Подпись — это HMAC-SHA256 от СЫРОГО тела запроса с секретом канала. Считайте её по полученным байтам, а не по заново собранному JSON: пересборка меняет пробелы, и подпись перестаёт сходиться.

// PHP
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);

if (!hash_equals($expected, $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

// Node
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) return res.sendStatus(401);

Как устроена доставка

  • Отвечайте любым 2xx. Всё остальное считается неудачей.
  • Три попытки с интервалом в полсекунды, дальше отправка сдаётся, а ошибка показывается на канале в кабинете.
  • Таймаут десять секунд — делайте долгую работу после ответа, а не до него.
  • Оставьте секрет пустым — он сгенерируется сам; без секрета заголовка с подписью просто не будет.
  • По умолчанию канал срабатывает только на отчёты от посетителей, которых сайт не опознал. Переключите на «все отчёты», если нужны и ваши собственные проверки.

Как получить ключ

Войдите в кабинет, откройте «Ключи API», нажмите «Новый ключ», выберите чтение или чтение и запись, при желании ограничьте проектами и задайте срок. Скопируйте сразу — ключ показывается один раз.

Перейти к ключам API