Konventionen#

Diese gelten für die ganze Schnittstelle. Wer das einmal liest, muss es nicht bei jeder Ressource neu herausfinden.

Versionierung#

Die Version steht im Pfad: /api/public/v1/.

Additive Änderungen — neue Felder, neue Endpunkte, neue optionale Parameter, neue Enum-Werte — kommen innerhalb von v1. Dein Client muss sie vertragen, und dafür braucht es zwei Gewohnheiten: ignoriere Antwortfelder, die du nicht kennst, und brich nicht bei einem unbekannten Enum-Wert ab.

Entfernungen, Umbenennungen, Typwechsel und alles, was die Validierung verschärft, erhöhen die Version, mit einem Überlappungsfenster, in dem beide laufen.

Antworten auf Listenabfragen#

Listen sind cursor-paginiert und kommen immer in derselben Hülle zurück:

{
  "object": "list",
  "data": [ { "object": "shift", "id": "0194d1c0-…", "…": "…" } ],
  "has_more": true,
  "next": "https://app.shiftavo.com/api/public/v1/shifts/?cursor=…",
  "previous": null
}
  • next und previous sind opake URLs. Folge ihnen unverändert; bau oder zerleg keine Cursor selbst.

  • Hör auf, wenn has_more false ist.

  • page_size steuert die Seitenlänge.

Der Unterscheider object#

Jede Ressource führt ein Feld "object", das ihren Typ nennt — "shift", "location", "employee". Verzweige darüber statt über die Form der Nutzlast, besonders in Webhook-Handlern und gemischten Sammlungen.

Fehler#

Eine Hülle, überall:

{
  "error": {
    "type": "conflict_error",
    "code": "not_cancellable",
    "message": "…",
    "details": []
  }
}
  • details ist bei jedem 4xx vorhanden — eine Liste der Fehler auf Feldebene, leer, wenn der Fehler keine Aufschlüsselung nach Feldern hat. Greif bei einem 5xx nicht ungeprüft per Index darauf zu.

  • param erscheint nur, wenn genau ein Feld schuld ist.

Es gibt neun Typen. Jeder ist genau einem Status zugeordnet, ausser invalid_request_error — dem Sammeltyp für die nicht tabellierten 4xx, bei dem der code den Unterschied trägt:

type

Status

Hinweise

invalid_request_error

400 · 405 · 406 · 415

code = invalid_request / method_not_allowed / not_acceptable / unsupported_media_type. Ein 405 heisst, dass die Ressource dieses Verb gar nicht anbietet — open_shifts und swaps sind schreibgeschützt — und nicht, dass deinem Token ein Scope fehlt

idempotency_error

400

Idempotency-Key mit einem anderen Body wiederverwendet (idempotency_key_reused)

authentication_error

401

fehlendes / ungültiges / abgelaufenes Token

plan_error

402

plan_required — das Abonnement des Unternehmens enthält diese Ressource nicht. Keine fehlende Berechtigung des Tokens; bis zum Planwechsel gibt es nichts zu wiederholen

permission_error

403

insufficient_scope und Verwandte

not_found

404

wird auch statt 403 für Datensätze ausserhalb deines Unternehmens zurückgegeben — kein Rückschluss auf die Existenz

conflict_error

409

Zustandskonflikte: duplicate, in_use, not_deletable, already_approved, already_confirmed, owner_protected, eine laufende idempotente Wiederholung

rate_limit_error

429

bringt Retry-After mit

api_error

5xx

auf unserer Seite: internal_error (500) / upstream_unavailable (502 — erneut versuchen)

Verzweige über type für die Behandlungsklasse und über code für den konkreten Fall. Beide sind stabil; message ist es nicht — behandle es nur als für Menschen lesbar.

Idempotenz#

Sende bei Erstellungen einen Idempotency-Key-Header. Eine wiederholte Anfrage spielt dann das erste Ergebnis erneut aus, statt einen zweiten Datensatz anzulegen.

Denselben Schlüssel mit einem anderen Body zu verwenden, ergibt einen 400 idempotency_key_reused — der Schlüssel bezeichnet eine Anfrage, nicht einen Endpunkt.

Anfragen nachverfolgen#

Jede Antwort, bei Erfolg wie bei Fehler, trägt einen X-Request-Id-Header. Protokollier ihn. Sendest du selbst ein X-Request-Id, wird es zurückgespiegelt, sodass eine Trace-ID von Ende zu Ende mitlaufen kann. Nenn sie in jeder Supportanfrage.

Übergänge im Lebenszyklus#

Ein Übergang wird als POST auf einen Aktions-Unterpfad ausgelöst: POST …/<id>/publish/, …/cancel/, …/revoke/.

Ein Übergang, der nicht möglich ist — einen Entwurf absagen, etwas bereits Veröffentlichtes veröffentlichen —, ist ein 409 mit einem eigenen Code (not_cancellable, not_publishable, not_unpublishable). Nie ein 200, das stillschweigend nichts getan hat.

Dasselbe gilt für Zeiterfassungen: POST …/timesheets/<id>/approve/ auf einer Zeile, die bereits freigegeben ist, ist ein 409 already_approved, und POST …/timesheets/<id>/confirm/ ohne Body auf Stunden, die eine Person bereits bestätigt hat, ist ein 409 already_confirmed (auf einer freigegebenen Zeile ist confirm/ ein 409 locked). Beides heisst, dass die Zeile schon dort ist, wo du sie haben wolltest — hol sie neu ab, statt es erneut zu versuchen.

Löschen#

DELETE gibt einen Stummel zurück, keinen 204:

{ "id": "0194d1c0-…", "object": "shift", "deleted": true }

Ein DELETE hat nie einen Body. Alles, was ein Löschvorgang braucht, steht im Pfad oder in der Query, sodass ein generierter Client alle Fälle ausdrücken kann. (HTTP lässt die Bedeutung eines DELETE-Bodys offen, und OpenAPI 3.0 sagt Konsumenten, einen solchen zu ignorieren — ein Body-Feld hier würde also stillschweigend aus deinem Client verschwinden und als 400 zurückkommen, den das Schema nicht erklären könnte.)

Löschen ist nicht das sanfte Verb#

Wo eine Ressource beides anbietet, sind sie keine Synonyme, und die falsche Wahl lässt sich nicht rückgängig machen:

Ressource

DELETE …/<id>/

Das sanfte Verb

shifts

entfernt die Schicht; die Zugeteilten einer veröffentlichten Schicht werden benachrichtigt, und ein laufendes Angebot in der Schichtbörse wird vorher zurückgezogen

POST …/cancel/ — die Schicht bleibt, als abgesagt markiert

employees

endgültige Entfernung: Zugang entzogen, die Person verschwindet aus jeder Sammlung, und ein späterer GET ist ein 404. Der Datensatz bleibt intern erhalten, damit Zeiterfassungen und Abwesenheiten ihre Verweise behalten. Der Inhaber des Unternehmens lässt sich weder entfernen noch deaktivieren: 409 owner_protected

POST …/deactivate/ (umkehrbar) · POST …/offboard/ (datierter Austritt + Abrechnung)

leave_requests

nur solange ausstehend oder abgelehnt — ein bestätigter oder widerrufener Antrag trägt eine Journalbuchung, deshalb ein 409 not_deletable, ebenso ein zurückgezogener

POST …/revoke/ — macht die Wirkung rückgängig, behält den Datensatz

Wiederkehrende Datensätze#

Das Löschen eines einzelnen Vorkommens eines wiederkehrenden shifts- oder availability-Datensatzes braucht einen ausdrücklichen Geltungsbereich:

DELETE …/<id>/?scope=this|following|all        # shifts
DELETE …/<id>/?scope=single|following|all      # availability

Die beiden Ressourcen behandeln einen weggelassenen Umfang unterschiedlich:

  • shifts — lässt du ihn bei einer Serie weg, bekommst du ein 409 delete_scope_required statt einer Vermutung, was du gemeint haben könntest.

  • availability — der Umfang ist standardmässig all: Lässt du ihn weg, wird der ganze Block gelöscht. single und following brauchen zusätzlich einen Anker occurrence=<ISO datetime>, der die gemeinte Wiederholung benennt; ein fehlender Anker ergibt ein 400.

Ratenbegrenzungen#

Ein 429 bringt einen Retry-After-Header mit. Halt dich daran und warte bei Wiederholungen exponentiell länger.