Webhooks
Вместо опроса вы можете попросить Accessful отправлять POST-запросом подписанное событие на ваш сервер, как только задача меняет состояние.
Как работает регистрация
Заголовок раздела «Как работает регистрация»Нет ни панели управления, ни эндпоинта регистрации. Вы прикрепляете обратный вызов на каждую загрузку и сами выбираете секрет для подписи — Accessful его не выдаёт.
- Multipart-загрузка — отправьте поля формы
webhookUrlиsecret. - Загрузка по URL — отправьте JSON-поля
callbackUrlиhmacSignature.
curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload" \ -H "X-API-Key: $ACCESSFUL_API_KEY" \ -F "files=@document.pdf" \ -F "webhookUrl=https://your-app.example.com/hooks/accessful" \ -F "secret=$YOUR_WEBHOOK_SECRET"События
Заголовок раздела «События»type события |
Срабатывает, когда |
|---|---|
case.queued |
PDF был принят и попал в очередь анализа. Срабатывает один раз и содержит стартовую позицию в очереди. |
case.running |
Задача начала обработку. Может срабатывать более одного раза (по одному на итерацию). |
case.completed |
Результат PDF/UA готов к скачиванию. |
case.failed |
Обработка не удалась. |
case.canceled |
Кейс был отменён. |
case.quota_exceeded |
Отклонено, потому что договорная квота исчерпана. |
Состояние quota_pending не доставляется как вебхук.
Payload
Заголовок раздела «Payload»Тело запроса — JSON. Content-Type: application/json.
{ "id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.completed", "apiVersion": "2026-06-26", "occurredAt": "2026-06-26T12:34:56Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "document.pdf", "jobStatus": "completed" }}| Поле | Тип | Примечания |
|---|---|---|
id |
UUID | ID события. Используйте его как ключ идемпотентности (см. ниже). |
type |
string | Один из типов событий выше. |
apiVersion |
string | Версия контракта (2026-06-26). Зафиксируйте её, чтобы отслеживать изменения. |
occurredAt |
ISO-8601 | Когда событие создано; стабильно между повторными попытками. |
data.caseId |
UUID | Кейс, к которому относится это событие. |
data.fileName |
string | Имя файла. |
data.jobStatus |
string | Сырой статус задачи, напр. queued, completed, failed, canceled, quota_exceeded. |
data.queuePosition |
integer | только case.queued — стартовый ранг в очереди, начиная с 1. |
data.queueTotal |
integer | только case.queued — общее число ожидающих кейсов на тот момент. |
data.estimatedWaitSeconds |
integer | только case.queued — приблизительная оценка ожидания в секундах. |
Null-поля опускаются в JSON.
Позиция в очереди при case.queued
Заголовок раздела «Позиция в очереди при case.queued»Событие case.queued — это push-аналог позиции в очереди из статуса задачи:
оно срабатывает один раз, в момент приёма PDF, и содержит стартовую позицию, чтобы вы могли
показать «вы 7-й в очереди, ~5 мин» без опроса.
{ "id": "a3b1c2d4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.queued", "apiVersion": "2026-06-26", "occurredAt": "2026-06-26T12:30:00Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "document.pdf", "jobStatus": "queued", "queuePosition": 7, "queueTotal": 23, "estimatedWaitSeconds": 300 }}Это дискретное событие жизненного цикла, а не живой поток — позиция представляет собой
снимок на момент приёма и не пересылается заново по мере продвижения очереди. Для непрерывно
обновляющейся позиции опрашивайте GET /job-status/{caseId},
ответ которого несёт те же поля queue*, пока кейс ожидает. Поля queue* появляются только в
case.queued; любое другое событие их опускает.
Заголовки доставки
Заголовок раздела «Заголовки доставки»| Заголовок | Пример | Назначение |
|---|---|---|
X-Accessful-Signature |
t=1749126896,v1=9f86d0… |
Подпись HMAC — проверяйте именно её |
X-Accessful-Webhook-Timestamp |
1749126896 |
Секунды Unix; то же, что t= выше |
X-Accessful-Event-Id |
f1d2c3b4-… |
Равен id; ключ идемпотентности |
X-Accessful-Event-Type |
case.completed |
Маршрутизация без разбора тела |
X-Accessful-Case-Id |
7c2f1e4a-… |
ID кейса |
X-Accessful-Delivery-Attempt |
1 |
Счётчик попыток, начиная с 1 |
X-Signature |
n4bQgY… |
legacy HMAC в base64 только по телу |
Проверка подписи
Заголовок раздела «Проверка подписи»Заголовок X-Accessful-Signature имеет вид t=<unix>,v1=<hex>. Пересчитайте её и сравните за
постоянное время:
- Прочитайте
tиv1из заголовка. - Вычислите
HMAC-SHA256(secret, "<t>.<сырое тело>")и закодируйте в hex. Подписываемая строка — это временная метка, буквальная точка, затем точное сырое тело запроса — проверяйте до разбора JSON. - Сравните за постоянное время с
v1. - При желании отклоняйте, если
tстарше нескольких минут (защита от повтора).
import crypto from 'node:crypto';
// `rawBody` должен быть точными полученными байтами (напр. express.raw()).function verify(rawBody, signatureHeader, secret) { const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('='))); const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 мин return valid && fresh;}String[] kv = signatureHeader.split(","); // ["t=...", "v1=..."]long t = Long.parseLong(kv[0].substring(2));String v1 = kv[1].substring(3);
Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] sig = mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8));String expected = HexFormat.of().formatHex(sig);
boolean valid = MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), v1.getBytes(StandardCharsets.UTF_8));Повторные попытки и идемпотентность
Заголовок раздела «Повторные попытки и идемпотентность»- Успех = любой ответ 2xx в пределах тайм-аута (5 с на подключение, 10 с на ответ). Отвечайте быстро, а свою работу выполняйте асинхронно.
- Сбой (не-2xx, тайм-аут или ошибка соединения) повторяется до 10 попыток с
экспоненциальной выдержкой и джиттером — примерно
10s → 30s → 1,5m → 4,5m → 13,5m → 40m → 2h, затем ограничивается 6h, минус до 20 % джиттера. Всё окно охватывает несколько часов. - После 10 неудачных попыток событие прекращают доставлять. Обратитесь в поддержку для повторной доставки.
- Идемпотентность: одно и то же событие может прийти более одного раза (напр. повторная
попытка после того, как ваш эндпоинт уже успешно ответил). Дедуплицируйте по
X-Accessful-Event-Id— он стабилен при каждой повторной попытке.