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», нажмите «Новый ключ», выберите чтение или чтение и запись, при желании ограничьте проектами и задайте срок. Скопируйте сразу — ключ показывается один раз.