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=…",
"previous": null
}
nextetprevioussont des URL opaques. Suis-les telles quelles ; ne construis ni n’analyse de curseurs.Arrête-toi quand
has_morevautfalse.page_sizecontrô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": []
}
}
detailsest présent sur chaque 4xx — une liste des échecs au niveau des champs, vide quand l’erreur n’a pas de détail par champ. N’y accède pas sans condition sur un 5xx.paramn’apparaît que lorsqu’un seul champ est en cause.
Il existe neuf 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 :
|
Statut |
Notes |
|---|---|---|
|
400 · 405 · 406 · 415 |
|
|
400 |
|
|
401 |
jeton manquant / invalide / expiré |
|
402 |
|
|
403 |
|
|
404 |
renvoyé aussi à la place d’un 403 pour les enregistrements hors de ton entreprise — aucune fuite d’existence |
|
409 |
conflits d’état : |
|
429 |
porte |
|
5xx |
de notre côté : |
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.
Il en va de même pour les feuilles de temps : POST …/timesheets/<id>/approve/ sur une ligne déjà approuvée donne un 409 already_approved, et POST …/timesheets/<id>/confirm/ sans corps sur des heures qu’une personne a déjà confirmées donne un 409 already_confirmed (sur une ligne approuvée, confirm/ donne un 409 locked). Dans les deux cas, la ligne est déjà dans l’état que tu voulais — récupère-la à nouveau plutôt que de réessayer.
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 |
|
Le verbe doux |
|---|---|---|
|
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 |
|
|
suppression terminale : accès révoqué, la personne quitte toutes les collections et un |
|
|
uniquement tant qu’elle est en attente ou refusée — une demande confirmée ou révoquée porte une écriture au journal, c’est donc un |
|
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
Les deux ressources traitent différemment une portée omise :
shifts— omets-la sur une série et tu obtiens un409 delete_scope_requiredplutôt qu’une supposition sur ton intention.availability— la portée vautallpar défaut : l’omettre supprime tout le bloc.singleetfollowingexigent en plus une ancreoccurrence=<ISO datetime>qui nomme l’occurrence visée ; une ancre manquante donne un400.
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.