Вебхуки
Каждое событие приходит на ваш адрес запросом POST с JSON:
POST /hooks/whiteboard HTTP/1.1
Content-Type: application/json
User-Agent: WhiteboardStudio-Webhooks/1.0
X-Whiteboard-Event: lesson.ended
X-Whiteboard-Delivery: 1842
X-Whiteboard-Signature: t=1760432400,v1=5f2c…
{"id":1842,"type":"lesson.ended","created_at":"2026-10-14T09:45:00+00:00","data":{"lesson_id":"6f1c…","external_id":"course-7/lesson-3","started_at":"…","ended_at":"…","minutes":45,"mode":"full"}}На любой ответ 2xx мы считаем событие доставленным. Отвечайте сразу, а обрабатывайте событие у себя в очереди.
События
type | data | Когда |
|---|---|---|
lesson.started | lesson_id, external_id, started_at, ended_at (null), minutes (0) | в уроке двое или больше, начали считаться минуты |
lesson.ended | то же + ended_at, minutes — минуты этого занятия, mode — с видео или без | 5 минут в уроке никого не было |
participant.joined | lesson_id, external_id, role, name | человек вошёл в урок. Приходит один раз за 6 часов на человека и урок |
balance.low | balance_seconds, balance_minutes | осталось меньше часа |
balance.exhausted | то же | минуты закончились |
webhook.test | message | вы вызвали POST /webhook/test |
id растёт и не повторяется, по нему отбрасывайте повторы. Порядок доставки не гарантирован: упорядочивайте по created_at.
Подпись
X-Whiteboard-Signature: t=<unix-время>,v1=<hex>, где v1 = HMAC-SHA256(secret, "<t>." + тело запроса). Секрет whsec_… возвращает PUT /webhook.
Проверяйте подпись по сырому телу запроса, до разбора JSON. Отклоняйте запросы старше 5 минут.
Node:
import { parseWebhook } from '@whiteboard-studio/partner'
app.post('/hooks/whiteboard', express.raw({ type: 'application/json' }), (req, res) => {
const event = parseWebhook({ secret: process.env.WB_WEBHOOK_SECRET!, header: req.get('X-Whiteboard-Signature'), body: req.body })
if (!event) return res.sendStatus(400)
queue.push(event)
res.sendStatus(204)
})Python (Django):
from whiteboard_partner import parse_webhook
def whiteboard_hook(request):
event = parse_webhook(settings.WB_WEBHOOK_SECRET, request.headers.get("X-Whiteboard-Signature"), request.body)
if event is None:
return HttpResponse(status=400)
queue.enqueue(event)
return HttpResponse(status=204)PHP:
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_WHITEBOARD_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, getenv('WB_WEBHOOK_SECRET'));
if (abs(time() - (int)$sig['t']) > 300 || !hash_equals($expected, $sig['v1'] ?? '')) { http_response_code(400); exit; }Повторы
Если ответ не 2xx или сервер не ответил за 10 секунд, мы повторяем отправку через 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 3 ч, 6 ч и 12 ч. Всего 9 попыток примерно за сутки, после этого событие считается недоставленным.
Перенаправления (3xx) мы не выполняем: укажите конечный адрес.
Требования к адресу
- Только
https://и только публичный адрес. Мы проверяем все IP-адреса имени и отправляем запрос именно на проверенный адрес. Адреса внутренних сетей не принимаются. - Сертификат должен быть действительным для имени из адреса.