Conventions#

Ces règles valent pour toute la surface. Les lire une fois évite de les redécouvrir ressource par ressource.

Versionnage#

La version est dans le chemin : /api/public/v1/.

Les changements additifs — nouveaux champs, nouveaux points de terminaison, nouveaux paramètres optionnels, nouvelles valeurs d’énumération — sont livrés à l’intérieur de v1. Ton client doit les tolérer, ce qui demande deux habitudes : ignorer les champs de réponse que tu ne reconnais pas, et ne pas échouer face à une valeur d’énumération inconnue.

Les suppressions, renommages, changements de type et tout ce qui durcit la validation font monter la version, avec une fenêtre de recouvrement pendant laquelle les deux sont actives.

Réponses de liste#

Les listes sont paginées par curseur et reviennent toujours dans la même enveloppe :

{
  "object": "list",
  "data": [ { "object": "shift", "id": "0194d1c0-…", "…": "…" } ],
  "has_more": true,
  "next": "https://app.shiftavo.com/api/public/v1/shifts/?cursor=…"
}
  • next est une URL opaque. Suis-la telle quelle ; ne construis ni n’analyse de curseurs.

  • Arrête-toi quand has_more vaut false.

  • page_size contrôle la longueur de la page.

Le discriminant object#

Chaque ressource porte un champ "object" qui nomme son type — "shift", "location", "employee". Branche dessus plutôt que sur la forme de la charge utile, en particulier dans les gestionnaires de webhooks et les collections mixtes.

Erreurs#

Une seule enveloppe, partout :

{
  "error": {
    "type": "conflict_error",
    "code": "not_cancellable",
    "message": "…",
    "details": [],
    "param": "start_date"
  }
}
  • details est toujours présent — une liste d’échecs au niveau des champs, vide quand l’erreur n’a pas de détail par champ.

  • param n’apparaît que lorsqu’un seul champ est en cause.

Il existe sept types. Chacun correspond à un statut, sauf invalid_request_error, le fourre-tout des 4xx non tabulés, où c’est le code qui porte la distinction :

type

Statut

Notes

invalid_request_error

400 · 405 · 406 · 415

code = invalid_request / method_not_allowed / not_acceptable / unsupported_media_type

idempotency_error

400

Idempotency-Key réutilisée avec un corps différent (idempotency_key_reused)

authentication_error

401

jeton manquant / invalide / expiré

permission_error

403

insufficient_scope, et consorts

not_found

404

renvoyé aussi à la place d’un 403 pour les enregistrements hors de ton entreprise — aucune fuite d’existence

conflict_error

409

conflits d’état : duplicate, in_use, not_deletable, un rejeu idempotent en cours

rate_limit_error

429

porte Retry-After

Branche sur type pour la classe de traitement, sur code pour le cas précis. Les deux sont stables ; message ne l’est pas — traite-le comme lisible par un humain uniquement.

Idempotence#

Envoie un en-tête Idempotency-Key sur les créations. Une requête réessayée rejoue alors le premier résultat au lieu de créer un second enregistrement.

Réutiliser une clé avec un corps différent est un 400 idempotency_key_reused — la clé identifie une requête, pas un point de terminaison.

Traçage des requêtes#

Chaque réponse, succès comme erreur, porte un en-tête X-Request-Id. Journalise-le. Envoie ton propre X-Request-Id et il t’est renvoyé, de sorte qu’un id de trace peut voyager de bout en bout. Cite-le dans toute demande de support.

Transitions de cycle de vie#

Une transition se déclenche par un POST vers un sous-chemin d’action : POST …/<id>/publish/, …/cancel/, …/revoke/.

Une transition qui ne peut pas s’appliquer — annuler un brouillon, publier quelque chose de déjà publié — est un 409 avec un code spécifique (not_cancellable, not_publishable, not_unpublishable). Ce n’est jamais un 200 qui n’a silencieusement rien fait.

Suppression#

DELETE renvoie un stub, pas un 204 :

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

Un DELETE ne prend jamais de corps. Tout ce dont une suppression a besoin se trouve dans le chemin ou la chaîne de requête, si bien qu’un client généré peut les exprimer toutes. (HTTP laisse indéfinie la sémantique d’un corps de DELETE et OpenAPI 3.0 dit aux consommateurs de l’ignorer ; un champ de corps ici disparaîtrait donc silencieusement de ton client et reviendrait sous forme d’un 400 que le schéma ne saurait expliquer.)

Supprimer n’est pas le verbe doux#

Là où une ressource offre les deux, ce ne sont pas des synonymes, et le mauvais choix n’est pas rattrapable :

Ressource

DELETE …/<id>/

Le verbe doux

shifts

supprime le shift ; les personnes assignées à un shift publié sont notifiées et toute offre active sur la bourse aux shifts est retirée au préalable

POST …/cancel/ — le shift reste, marqué annulé

employees

suppression terminale : accès révoqué, la personne quitte toutes les collections et un GET ultérieur donne un 404. L’enregistrement est conservé en interne pour que les rapports d’heures et les absences gardent leurs références

POST …/deactivate/ (réversible) · POST …/offboard/ (fin de contrat datée + solde de tout compte)

leave_requests

uniquement tant qu’elle est en attente ou refusée — une demande confirmée ou révoquée porte une écriture au grand livre, c’est donc un 409 not_deletable

POST …/revoke/ — annule l’effet, conserve l’enregistrement

Enregistrements récurrents#

Supprimer une occurrence d’un enregistrement shifts ou availability récurrent nécessite une portée explicite :

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

Omets-la sur une série et tu obtiens un 409 delete_scope_required plutôt qu’une supposition sur ton intention.

Limites de débit#

Un 429 porte un en-tête Retry-After. Respecte-le, et applique un back-off exponentiel en cas de répétition.