Webhooky
Místo pravidelného pollování GET /api/v1/events se můžeš přihlásit k odběru vybraných událostí na vlastní URL - onDuty na ni pošle POST okamžitě, jakmile událost nastane.
Registrace
curl -X POST https://www.onduty.cz/api/v1/webhooks \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://tvuj-server.cz/onduty-webhook","events":["shift.published","punch.created"]}'URL se PŘED založením OVĚŘÍ - onDuty na ni pošle testovací webhook.verify událost a čeká 2xx odpověď. Bez ní se webhook nezaloží (422 webhook-verify-failed). Odpověď obsahuje secret - zobrazí se JEN TEĎ, ulož si ho, budeš ho potřebovat k ověření podpisu.
Ověření podpisu
Každé doručení nese hlavičky X-OnDuty-Timestamp a X-OnDuty-Signature - podpis je HMAC-SHA256 ze surového těla požadavku spojeného s časovým razítkem:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyOnDutySignature(
rawBody: string,
timestamp: string,
signature: string,
secret: string,
): boolean {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// V handleru tvého serveru (Express/Next.js/...):
export async function POST(req: Request) {
const rawBody = await req.text(); // SUROVÉ tělo - podpis počítáme
// z přesného bajtového obsahu, ne z
// req.json() (ten by tělo znovu
// serializoval a podpis by nesedel)
const timestamp = req.headers.get("x-onduty-timestamp") ?? "";
const signature = req.headers.get("x-onduty-signature") ?? "";
const secret = process.env.ONDUTY_WEBHOOK_SECRET!;
if (!verifyOnDutySignature(rawBody, timestamp, signature, secret)) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody) as { type: string; occurred_at: string; data: unknown };
// ... zpracuj event.type (např. "shift.published") a event.data
return new Response("ok", { status: 200 });
}Retry a vypnutí
Neúspěšné doručení (timeout, ne-2xx odpověď) se opakuje s odstupňovaným odstupem: 1, 5, 15, 60, 240 minut, pak každé 4 hodiny. Po 20 neúspěšných pokusech v řadě se webhook automaticky VYPNE (active: false) - oprav svůj endpoint a webhook znovu zaregistruj, nebo požádej centrálu o reaktivaci v portálu (sekce API zobrazuje log posledních 50 doručení).
Typy událostí
employee.created, employee.updated, employee.deactivated, shift.published, shift.updated, shift.deleted, punch.created, timesheet.flagged, timesheet.approved, absence.requested, absence.approved, absence.rejected, month.locked.
Doplněné o historii změn (provozní feedback 2): shift.created, shift.assigned, shift.unassigned, absence.created, absence.deleted, employment.created, employment.updated, employment.deactivated, employment.reactivated, month.unlocked, day_note.updated, timesheet.adjusted, user.role_changed.
Doplněné o storno pípnutí (absence v Docházce): punch.voided - vedoucí ve své Docházce stornoval(a) omylem zapsané pípnutí, dřívější punch.created tím pozbylo platnost.