Suivi et validation
Les quatre statuts d’un colis, la réponse du client, le jeton d’expéditeur et les routes de l’accusé de réception.
Chaque colis créé par la page de livraison, depuis le navigateur, la CLI ou n8n, reçoit un accusé de réception : quand il a été envoyé, quand le client l’a ouvert pour la première fois, et ce qu’il a répondu.
Les statuts
status | Affiché | Signifie |
|---|---|---|
sent | envoyé | Le colis existe ; le client ne l’a pas encore ouvert. |
opened | ouvert | Le client l’a ouvert : la page de retrait, un téléchargement ou une réponse. |
approved | validé | Le client a validé la livraison. |
changes | à corriger | Le client a demandé des corrections, avec un commentaire. |
Les libellés sont les mêmes partout : sur la page de retrait, sur la page d’envoi, dans
colis statut et dans le nœud n8n.
Ce qui compte comme une ouverture
- La page de retrait, une fois affichée dans le navigateur. L’ouverture est signalée depuis le navigateur après le rendu, donc un robot qui charge le lien pour en faire un aperçu dans une messagerie ne compte pas.
- Un téléchargement de
/raw, puisquecolis recevoirne charge jamais la page. - Une réponse : répondre suppose d’avoir ouvert, même si le signal d’ouverture s’est perdu.
Seule la première ouverture est enregistrée, par une écriture conditionnelle : les suivantes ne
changent rien et n’envoient pas de nouveau parcel.opened.
Un téléchargement fait par un outil (n8n, un script) est vu comme une ouverture par le client : le déploiement ne peut pas les distinguer.
L’expéditeur n’est pas le client
Créer un colis renvoie un jeton d’expéditeur : 32 octets aléatoires, dans l’en-tête
x-colis-sender-token de la réponse, et dans un cookie HttpOnly pour le navigateur qui a envoyé,
valable aussi longtemps que le colis. Seule son empreinte SHA-256 est stockée.
Tant que ce jeton accompagne la requête (le cookie, ou l’en-tête pour un client qui n’est pas un navigateur), le déploiement sait que c’est l’expéditeur :
- votre propre visite sur le lien ne compte pas comme une ouverture ;
- vous ne pouvez pas répondre à la place du client (
403) ; - la page de retrait vous montre l’accusé plutôt que les boutons de réponse.
La page d’envoi interroge le statut toutes les cinq secondes tant que l’onglet est visible, et
s’arrête à la réponse du client. Une fois l’onglet fermé, colis statut ou un
webhook prennent le relais.
La réponse du client
Sous l’aperçu, deux boutons : Valider la livraison et Demander des corrections. Demander des corrections exige un commentaire (jusqu’à 1 000 caractères) : « ça ne va pas » sans dire quoi n’est pas une réponse sur laquelle on peut agir. Le commentaire est facultatif avec une validation.
La réponse est définitive : une seconde est refusée plutôt qu’écrite par-dessus la première, ce qui lui donne sa valeur d’accusé. Pour une nouvelle version, envoyez un nouveau colis : chaque version a son code et son accusé.
Où lire l’accusé
- La page d’envoi, juste après le dépôt, et la page de retrait ouverte par l’expéditeur.
- La CLI :
colis statut <code> --remote …. - n8n : l’opération Get Status du nœud Colis, ou le déclencheur sur chaque événement.
- Les webhooks :
parcel.opened,parcel.approved,parcel.changes_requested.
Les routes
Elles s’ajoutent aux quatre routes du protocole, sous /api/transfers :
| Route | Corps | Réponse |
|---|---|---|
GET /:code/status | — | 200 { status, sentAt, openedAt?, decidedAt?, comment?, isSender } |
POST /:code/opened | { device? }, facultatif | 204. Ignoré quand c’est l’expéditeur qui appelle. |
POST /:code/verdict | { decision: 'approved' | 'changes', comment? } | 201 avec le nouvel accusé. |
{
"status": "changes",
"sentAt": "2026-09-22T10:00:00.000Z",
"openedAt": "2026-09-22T11:00:00.000Z",
"decidedAt": "2026-09-22T12:00:00.000Z",
"comment": "Le logo en SVG, et le fond plus clair.",
"isSender": false
}Les erreurs gardent la forme du protocole, { error: { code, message } } :
| Code | HTTP | Quand |
|---|---|---|
INVALID_REQUEST | 400 | Corps illisible, décision inconnue, corrections sans commentaire, commentaire trop long. |
UNAUTHORIZED | 401 | Le colis est protégé et le mot de passe n’a pas été donné. |
FORBIDDEN | 403 | L’expéditeur essaie de répondre à la place du client. |
NOT_FOUND | 404 | Aucun accusé pour ce code : inconnu, jamais suivi, ou accusé expiré. |
ALREADY_DECIDED | 409 | Le colis a déjà reçu une réponse. |
Pour un colis protégé, ces routes demandent le même mot de passe que le reste : le cookie posé par
/api/unlock/<code>, ou le mot de passe en jeton Bearer.
Les versions
Une demande de corrections n’est pas la fin : l’expéditeur répond par une nouvelle version, sous le même code.
colis envoyer ./maquette-v2.pdf --remplace K7QP2M4XLe client rouvre le même lien. Il y voit « Version 2 sur 2 », l’aperçu de la nouvelle version, les deux boutons, et en dessous l’historique : chaque version précédente, avec sa date, la réponse reçue et le commentaire. Seule la version en cours se télécharge.
Les règles :
- seul l’expéditeur envoie une version, prouvé par son jeton (le cookie de la page, ou
x-colis-sender-tokenpour la CLI et les API) ; - seulement après « corrections demandées » : sans réponse, ou après une validation, la route
répond
409(AWAITING_VERDICT,ALREADY_APPROVED) ; - au plus
DROP_MAX_VERSIONSversions par code (10 par défaut,409 VERSION_LIMIT) ; - chaque version est un envoi comme un autre : mêmes tailles, mêmes durées au choix, et, sur un déploiement ouvert, elle compte dans les envois par heure ;
- elle garde le mot de passe du code ; un code à usage unique n’en reçoit pas (
409 ONE_TIME_CODE).
Chaque version repart avec sa propre durée de vie. Les routes de lecture (GET /:code, /raw,
l’aperçu) et celles du suivi (/status, /opened, /verdict) portent sur la version en cours ;
/status y ajoute version et versions, l’historique complet.
| Route | Corps | Réponse |
|---|---|---|
POST /api/transfers/:code/versions | le fichier, comme POST /api/transfers | 201, le colis avec version |
POST /api/transfers/uploads | { …, replaces: "<code>" }, puis parts et fin | comme un gros fichier, sous le même code |
Rien de ce que la première version a écrit n’est réécrit : les suivantes vont sous
v/<code>/<n>, avec leurs propres meta/<code>.v<n>.opened et .verdict, et un index
meta/<code>.versions qui tient la version en cours et l’historique. Cet index est réécrit par
écriture conditionnelle (ETag) : de deux versions envoyées en même temps, une seule passe, l’autre
reçoit 409 VERSION_CONFLICT.
Combien de temps l’accusé vit
L’accusé est rangé à côté du colis, sous meta/<code>.delivery, .opened et .verdict dans le
préfixe du déploiement. Il ne disparaît pas quand le colis est détruit ou expire : il vit aussi
longtemps que la plus longue durée autorisée, DROP_MAX_EXPIRES_IN. C’est ce qui permet de voir la
validation d’un colis à usage unique déjà téléchargé.
Avec des versions, l’index meta/<code>.versions repart pour DROP_MAX_EXPIRES_IN à chaque
nouvelle version : l’accusé vit au moins aussi longtemps que la plus récente.
Votre règle de cycle de vie supprime aussi ces objets : réglez-la au-delà de DROP_MAX_EXPIRES_IN,
comme décrit dans Page de livraison.
Un colis envoyé par la CLI sans --remote, directement dans le bucket, n’a pas d’accusé : c’est
le déploiement qui le tient.
Webhooks & n8n
Un événement signé à chaque étape d’un colis : envoyé, ouvert, validé, à corriger, supprimé. Le vérifier, et le brancher sur n8n.
Gros fichiers
Au-delà de DROP_MAX_SIZE_MB, le navigateur envoie le fichier directement au bucket, en morceaux, avec reprise. La règle CORS et le nettoyage qu’il faut.