Protokół webhook

Kształt payloadu, podpisywanie HMAC, polityka ponowień i wyłącznik bezpieczeństwa dla webhooków wychodzących.

Gdy zadanie osiąga stan terminalny (completed, failed, cancelled), dispatcher wysyła JSON body na callback_url instalacji.

Payload

{
  "event": "job.completed",
  "delivered_at": "2026-05-09T08:00:00.000Z",
  "job": {
    "id": "job_01h82k…",
    "status": "completed",
    "order_id": "ord_01h82k…",
    "created_at": "2026-05-09T07:58:14.000Z"
  },
  "outputs": [
    {
      "url": "https://storage.qamera.ai/…?expires=…",
      "type": "image/png",
      "width": 1024,
      "height": 1024
    }
  ],
  "external_metadata": { "your": "tag" }
}

outputs[].url jest presigned na 7 dni (zgodnie z POST /jobs/{id}/refresh-url). Odśwież przez ten endpoint, jeśli potrzebujesz świeżego URL po wygaśnięciu.

Podpisywanie

Każda delivery niesie:

X-Qamera-Signature: t=<unix-seconds>,v1=<hex_hmac_sha256>

Podpisywany string to <t>.<raw-body>. Odrzucaj podpisy, w których t jest starsze niż 5 minut.

Weryfikacja w Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, headerValue, secret) {
  const parts = Object.fromEntries(
    headerValue.split(',').map((p) => p.split('=')).map(([k, v]) => [k, v]),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));
}

Weryfikacja w PHP

function qamera_verify($rawBody, $header, $secret) {
  parse_str(strtr($header, ',', '&'), $parts);
  if (!isset($parts['t']) || !isset($parts['v1'])) return false;
  if (abs(time() - (int)$parts['t']) > 300) return false;
  $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
  return hash_equals($expected, $parts['v1']);
}

Okno karencji rotacji

Po rotacji sekretu HMAC przez POST /installations/{id}/rotate-hmac poprzedni sekret pozostaje ważny przez 48h. W tym oknie deliveries niosą dwa segmenty v1= (po jednym na sekret); akceptuj którykolwiek. Przykładowy nagłówek:

X-Qamera-Signature: t=1746777600,v1=e3b0…,v1=44a0…

Po zakończeniu okna karencji tylko nowy sekret jest podpisywany.

Gwarancje niezawodności

  • Do 8 prób, exponential backoff z jitter, cap 1 godzina.
  • 5 kolejnych failów wstrzymuje deliveries na 30 minut (wyłącznik bezpieczeństwa).
  • 3 cykle wstrzymania zawieszają instalację; admin musi ją odzawiesić.
  • Po wszystkich próbach delivery jest oznaczona jako abandoned i może być powtórzona przez POST /webhooks/{delivery_id}/replay.

Twój endpoint powinien:

  1. Zwrócić dowolne 2xx dla zaakceptowanych eventów.
  2. Procesować eventy idempotentnie — dispatcher może ponowić po wolnym 200.
  3. Persystować (event, job_id, delivered_at) dla audytu; dispatcher sam z siebie nie wysyła ponownie potwierdzonych eventów.