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.running | Задача начала обработку. Может срабатывать более одного раза (по одному на итерацию). |
case.completed | Результат PDF/UA готов к скачиванию. |
case.failed | Обработка не удалась. |
case.canceled | Кейс был отменён. |
case.quota_exceeded | Отклонено, потому что договорная квота исчерпана. |
Состояния queued и quota_pending не доставляются как вебхуки.
Payload
Заголовок раздела «Payload»Тело запроса — JSON. Content-Type: application/json.
{ "id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.completed", "apiVersion": "2026-06-05", "occurredAt": "2026-06-05T12:34:56Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "document.pdf", "jobStatus": "completed" }}| Поле | Тип | Примечания |
|---|---|---|
id | UUID | ID события. Используйте его как ключ идемпотентности (см. ниже). |
type | string | Один из типов событий выше. |
apiVersion | string | Версия контракта (2026-06-05). Зафиксируйте её, чтобы отслеживать изменения. |
occurredAt | ISO-8601 | Когда событие создано; стабильно между повторными попытками. |
data.caseId | UUID | Кейс, к которому относится это событие. |
data.fileName | string | Имя файла. |
data.jobStatus | string | Сырой статус задачи, напр. completed, failed, canceled, quota_exceeded. |
Null-поля опускаются в JSON.
Заголовки доставки
Заголовок раздела «Заголовки доставки»| Заголовок | Пример | Назначение |
|---|---|---|
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— он стабилен при каждой повторной попытке.