In sechs Schritten angebunden
Diese Anleitung führt von der Key-Erstellung bis zur produktionsreifen Synchronisation des Dienstplans. Alle Codebeispiele lassen sich direkt kopieren.
1. API-Key erzeugen
Der API-Key wird im VetTime-Konto des Mandanten erzeugt und ist ausschließlich für diesen Mandanten gültig. Keys beginnen mit vt_live_. Falls ihr noch keinen Zugang habt, meldet euch über das Kontaktformular.
Legt den Key als Umgebungsvariable in eurem Backend ab, niemals im Frontend-Bundle oder in der Versionskontrolle.
2. Mitarbeiter-Mapping in VetTime hinterlegen
Die API übergibt Mitarbeitende über employee.external_id. Dafür muss im VetTime-Konto des Mandanten pro Mitarbeitendem eine Zuordnung von der internen VetTime-ID zur ID im angebundenen System (z. B. PMS-Benutzer-ID) gepflegt werden.
Ohne dieses Mapping ist external_id null. Ein Abgleich über Namen ist nicht erlaubt, da VetTime interne IDs nie ausliefert.
3. Ersten Request senden
Ein minimaler Aufruf braucht nur from und to. Beide Daten sind inklusiv und beziehen sich auf Europe/Berlin.
export VETTIME_API_KEY='vt_live_...'
curl --fail-with-body \
--get 'https://app.vettime.de/api/public/v1/shifts' \
--header "Authorization: Bearer ${VETTIME_API_KEY}" \
--data-urlencode 'from=2026-08-17' \
--data-urlencode 'to=2026-08-31'4. Paginierung über den Cursor
Solange meta.has_more den Wert true hat, ruft ihr denselben Endpunkt mit meta.next_cursor auf. Alle übrigen Parameter bleiben unverändert, der Cursor ist undurchsichtig.
async function* fetchAllShifts(from, to, apiKey) {
let cursor = null;
do {
const url = new URL('https://app.vettime.de/api/public/v1/shifts');
url.searchParams.set('from', from);
url.searchParams.set('to', to);
url.searchParams.set('limit', '250');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const body = await res.json();
yield* body.data;
cursor = body.meta.has_more ? body.meta.next_cursor : null;
} while (cursor);
}5. Synchronisation als Snapshot
Die API ist kein Event-Stream. Sammelt den vollständigen Zeitraum ein und schreibt erst danach atomar. Da Zuweisungs-IDs nach erneutem Veröffentlichen wechseln können, braucht es regelmäßig einen vollständigen Abgleich statt reiner Deltas.
// 1. Vollständigen Snapshot einsammeln
const snapshot = [];
for await (const shift of fetchAllShifts(from, to, apiKey)) {
snapshot.push(shift);
}
// 2. Erst nach dem letzten Seitenabruf schreiben
await db.transaction(async (tx) => {
const ids = snapshot.map((s) => s.id);
await tx.upsertShifts(snapshot);
// 3. Zuvor importierte Zuweisungen im Zeitraum entfernen,
// die im aktuellen Snapshot fehlen
await tx.deleteShiftsInRangeExcept(from, to, ids);
});6. Fehler robust behandeln
Bei 429 gilt Retry-After. Bei 5xx wird mit exponentiellem Backoff plus Jitter wiederholt. 400 und 401 sind nicht wiederholbar und gehören ins Log samt X-Request-Id.
async function requestWithRetry(url, apiKey, attempt = 0) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
await sleep(retryAfter * 1000);
return requestWithRetry(url, apiKey, attempt);
}
if (res.status >= 500 && attempt < 5) {
const backoff = 2 ** attempt * 500 + Math.random() * 250; // Jitter
await sleep(backoff);
return requestWithRetry(url, apiKey, attempt + 1);
}
if (!res.ok) {
const requestId = res.headers.get('X-Request-Id');
const body = await res.text();
throw new Error(`VetTime API ${res.status} (request ${requestId}): ${body}`);
}
return res.json();
}7. Keys sicher betreiben
Keys niemals in URLs, Browser- oder Mobile-Code, Logs, E-Mails oder Chats übergeben. Rotiert Keys bei Personalwechsel und bei Verdacht auf Kompromittierung, ein ersetzter Key ist sofort ungültig. Beim Support-Kontakt nur X-Request-Id und UTC-Zeitpunkt nennen, nie den Key selbst.
Vollständiges Antwortschema, Feldsemantik, Fehlercodes und Limits.
Zur API-Referenz