# Webhooks & n8n

> Un événement signé à chaque étape d’un colis : envoyé, ouvert, validé, à corriger, supprimé. Le vérifier, et le brancher sur n8n.

Canonical: https://colis-docs.vercel.app/docs/webhooks · Markdown: https://colis-docs.vercel.app/docs/webhooks.md

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](https://colis-docs.vercel.app/docs/page-de-livraison), deux variables :

```sh
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 32
```

`DROP_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](https://colis-docs.vercel.app/docs/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 :

```json
{
  "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](https://www.standardwebhooks.com) :

```http
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 :

```ts title="app/api/colis/route.ts"
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`](https://colis-docs.vercel.app/docs/cli#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

1. **Identifiants.** Créez des identifiants **Colis API** :
   - **Base URL** : le déploiement, par exemple `https://votre-livraison.vercel.app`.
   - **Upload Password** : le `DROP_PASSWORD` du déploiement, s’il en a un. Seul **Send** s’en sert.
   - **Webhook Secret** : le `DROP_WEBHOOK_SECRET` du déploiement. Seul le déclencheur s’en sert.
2. **Le déclencheur.** Ajoutez un **Colis Trigger**, choisissez les événements (tous par défaut) et
   copiez sa **Production URL**.
3. **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 dans `DROP_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](https://colis-docs.vercel.app/docs/cas-d-usage/facturer-a-la-validation) et
[un résumé IA de chaque livrable](https://colis-docs.vercel.app/docs/cas-d-usage/resume-ia-des-livrables).
