Dokumentace API

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.