Webhooks & n8n
Un événement signé à chaque étape d’un colis : envoyé, ouvert, validé, à corriger, supprimé. Le vérifier, et le brancher sur n8n.
Chaque étape de la vie d’un colis peut lancer une automatisation : vous prévenir qu’il a été ouvert,
déplacer une carte, envoyer la facture une fois la livraison validée, faire lire le document par une
IA. La page de livraison publie ces événements ; n’importe quelle route qui accepte un POST peut
les recevoir.
Les activer
Sur le déploiement de la page de livraison, deux variables :
DROP_WEBHOOK_URL=https://n8n.example.com/webhook/colis,https://votre.app/api/colis
DROP_WEBHOOK_SECRET=… # 32 caractères ou plus : openssl rand -base64 32DROP_WEBHOOK_URL prend une adresse ou plusieurs séparées par des virgules. Le secret est
obligatoire dès qu’une adresse est définie, et doit faire au moins 32 caractères : sinon le
déploiement refuse de démarrer. Redéployez après avoir changé l’une ou l’autre.
Les événements
| Événement | Quand |
|---|---|
parcel.sent | Un colis a été créé, par POST /api/transfers ou par un envoi de gros fichier terminé. |
parcel.opened | Le client l’a ouvert : la page de retrait, un téléchargement ou une réponse. La première fois seulement. |
parcel.approved | Le client a validé la livraison. |
parcel.changes_requested | Le client a demandé des corrections ; data.comment dit lesquelles. |
parcel.revised | L’expéditeur a répondu par une nouvelle version, sous le même code et le même lien. |
parcel.deleted | Le code a été détruit : DELETE, ou le téléchargement d’un colis à usage unique. |
L’expéditeur qui ouvre son propre lien ne déclenche pas parcel.opened : il est reconnu par son
jeton d’expéditeur (voir Suivi et validation). Un colis qui expire
simplement n’envoie rien.
Le contenu
Chaque événement est un POST en JSON, envoyé après la réponse, donc personne ne l’attend :
{
"id": "5f0c6f7e-…",
"type": "parcel.approved",
"createdAt": "2026-09-22T12:00:00.000Z",
"data": {
"code": "K7QP2M4X",
"url": "https://votre-livraison.vercel.app/K7QP2M4X",
"filename": "maquette-v2.pdf",
"size": 1843200,
"contentType": "application/pdf",
"expiresAt": "2026-09-29T10:00:00.000Z",
"note": "La version avec le logo corrigé",
"device": "Chrome sur macOS",
"protected": false,
"oneTime": false,
"status": "approved",
"sentAt": "2026-09-22T10:00:00.000Z",
"openedAt": "2026-09-22T11:00:00.000Z",
"decidedAt": "2026-09-22T12:00:00.000Z",
"version": 1
}
}data décrit le colis au moment où l’événement part ; status vaut sent, opened, approved ou
changes. data.version dit de quelle version il s’agit : 1 pour le premier fichier, puis une de
plus à chaque parcel.revised. Un workflow sur .changes_requested et .revised suit ainsi tous
les allers-retours. data.url est la page de retrait, construite depuis DROP_URL ou, à défaut, le domaine
de production Vercel. data ne contient jamais le mot de passe, son empreinte ni le jeton de
l’expéditeur. Le message (note) disparaît des événements avec le colis ; le nom, la taille et le
type sont gardés avec l’accusé, donc une validation arrivée après un téléchargement unique nomme
encore le fichier.
Les types TypeScript sont exportés par @colis/protocol : ColisWebhookEvent, un type par
événement (ColisParcelApprovedEvent…), ColisParcelData, et la liste COLIS_WEBHOOK_EVENT_TYPES
avec isColisWebhookEventType().
La signature
Les événements sont signés selon Standard Webhooks :
POST https://votre.app/api/colis
content-type: application/json
webhook-id: 5f0c6f7e-…
webhook-timestamp: 1790078400
webhook-signature: v1,<base64 du HMAC-SHA256 de "${id}.${timestamp}.${body}">La clé est le secret en UTF-8, ou ses octets décodés pour un secret écrit whsec_<base64>, que les
bibliothèques officielles vérifient alors telles quelles. Vérifiez avec la fonction qui signe :
import { verifyWebhook, type ColisWebhookEvent } from '@colis/protocol'
export async function POST(request: Request) {
const body = await request.text() // le corps brut, avant tout JSON.parse
const result = await verifyWebhook(process.env.DROP_WEBHOOK_SECRET!, request.headers, body)
if (!result.valid) return new Response(null, { status: 401 }) // result.reason dit pourquoi
const event = JSON.parse(body) as ColisWebhookEvent
if (event.type === 'parcel.changes_requested') {
// event.data.comment : ce que le client a écrit
}
return new Response(null, { status: 204 })
}result.reason vaut missing-headers, invalid-timestamp, stale-timestamp ou
invalid-signature. Un horodatage décalé de plus de cinq minutes (WEBHOOK_TOLERANCE_SECONDS) est
refusé, pour qu’une ancienne livraison ne puisse pas être rejouée ; toleranceSeconds en option
change cette marge. signWebhook() et webhookHeaders() produisent la même signature, pour tester
votre récepteur.
Les nouvelles tentatives
Chaque événement est tenté trois fois au plus, à 0,5 s puis 2 s d’intervalle, cinq secondes chacune.
Une réponse 4xx autre que 408 et 429 n’est pas retentée, et une redirection n’est pas suivie.
Un récepteur qui reste en panne rate l’événement : il est journalisé avec son origine, jamais son
chemin ni le secret, et aucun envoi ni téléchargement n’échoue à cause de lui.
Dédupliquez sur webhook-id : une nouvelle tentative porte le même. Et comme rien n’est rejoué après
la troisième, gardez colis statut pour vérifier.
Avec n8n
packages/n8n-nodes-colis fournit deux nœuds et un type d’identifiants :
| Nœud | Rôle |
|---|---|
| Colis Trigger | Lance le workflow sur parcel.sent, .opened, .approved, .changes_requested, .revised ou .deleted. |
| Colis | Sur un colis : Send un fichier binaire, Get ses métadonnées, Get Status de sa livraison, Delete. |
L’installer
Dans n8n : Settings → Community nodes → Install, tapez n8n-nodes-colis et confirmez. Les deux
nœuds et les identifiants Colis API apparaissent ensuite dans le panneau des nœuds.
Sur une instance auto-hébergée sans cet écran, lancez npm install n8n-nodes-colis dans
~/.n8n/nodes, puis redémarrez n8n.
Le brancher
- Identifiants. Créez des identifiants Colis API :
- Base URL : le déploiement, par exemple
https://votre-livraison.vercel.app. - Upload Password : le
DROP_PASSWORDdu déploiement, s’il en a un. Seul Send s’en sert. - Webhook Secret : le
DROP_WEBHOOK_SECRETdu déploiement. Seul le déclencheur s’en sert.
- Base URL : le déploiement, par exemple
- Le déclencheur. Ajoutez un Colis Trigger, choisissez les événements (tous par défaut) et copiez sa Production URL.
- Le déploiement. La page de livraison n’a pas d’API d’abonnement : collez cette adresse dans
DROP_WEBHOOK_URL, mettez le même secret dansDROP_WEBHOOK_SECRET, redéployez, puis activez le workflow.
Avec un secret dans les identifiants, le déclencheur vérifie chaque signature avec verifyWebhook
et répond 401 à toute requête non signée, falsifiée ou vieille de plus de cinq minutes. Un
événement que vous n’avez pas coché reçoit 200 et ne lance rien, pour que le déploiement ne le
retente pas. Sans secret, tout est pris sur parole : gardez-en un.
Le nœud Colis
| Opération | Requête | Renvoie |
|---|---|---|
| Send | POST /api/transfers, le fichier en corps | code, size, expiresAt, et url : la page de retrait |
| Get | GET /api/transfers/:code | Nom, taille, type, expiration, message, oneTime, protected |
| Get Status | GET /api/transfers/:code/status | sent, opened, approved ou changes, avec les heures |
| Delete | DELETE /api/transfers/:code | { code, deleted: true } |
Send prend le fichier dans un champ binaire (data par défaut) et les options de la page : durée
de vie, message, mot de passe, destruction au premier téléchargement, nom du fichier, étiquette
d’appareil (n8n par défaut). Il envoie en une seule requête : le fichier doit tenir sous
DROP_MAX_SIZE_MB. Pour un colis protégé, remplissez Parcel Password sur les trois autres
opérations.
Deux workflows complets sont décrits dans les cas d’usage : facturer dès la validation et un résumé IA de chaque livrable.