# Enregistrer un webhook

L'API v1 n'applique aucune limite de webhooks par boutique — enregistrez autant d'URLs que votre intégration en a réellement besoin, et supprimez celles que vous n'utilisez plus. (L'[addon Webhooks marchand](https://dzbuild.com/fr/fr/api-docs/webhooks/overview.md) sans code, lui, plafonne bien les endpoints : 1 en Unlimited, 3 en Enterprise.)

Chaque webhook peut s'abonner à une liste différente d'événements ; choisissez un modèle qui vous convient :

* **Un seul endpoint, tous les events** — le plus simple pour des petites apps. Branchez sur `event` dans le handler.
* **Plusieurs endpoints, un event chacun** — plus propre en microservices, mais plus d'URLs à gérer.

Accès pilote

Les webhooks API v1 demandent une clé API du marchand, d'une boutique avec un plan **Enterprise** actif ; tout autre plan, ou un abonnement expiré, reçoit `403 forbidden` `"API access requires an active Enterprise plan"`. Les clés générées sur `/dashboard/api` sont déjà inscrites au pilote ; une clé non inscrite reçoit `403 forbidden` `"API is in pilot mode; key not enrolled"`. Les jetons d'application installée reçoivent `403 forbidden` `"Apps cannot use this endpoint"` sur chaque appel sous `/v1/webhooks`.

## `POST /v1/webhooks` — enregistrer[​](#post-v1webhooks--enregistrer "Lien direct vers post-v1webhooks--enregistrer")

**Auth :** clé plateforme avec `webhooks:write`. **Nécessite `Idempotency-Key`.** Contrairement à la plupart des écritures, cette réponse n'est jamais mise en cache pour le rejeu, car elle contient le secret : réessayer avec la même clé enregistre un second webhook avec un nouveau secret. Si un appel expire, vérifiez `GET /v1/webhooks` avant de réessayer et supprimez tout doublon.

### Corps[​](#corps "Lien direct vers Corps")

| Champ    | Type               | Requis | Notes                                                                                                                                                                                  |
| -------- | ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`    | string (URL https) | ✅     | Doit être `https://` ; une URL `http://` est refusée. La longueur maximale est de 500 caractères ; une URL plus longue est rejetée en `500 server_error`, pas en erreur de validation. |
| `events` | string\[]          | ✅     | Liste de noms d'events. Voir [Catalogue d'events](https://dzbuild.com/fr/fr/api-docs/webhooks/events.md). Vide = erreur.                                                               |

### Requête[​](#requête "Lien direct vers Requête")

```
curl -X POST 'https://api.dzbuild.app/v1/webhooks' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Content-Type: application/json" \

  -H "Idempotency-Key: $(uuidgen)" \

  -d '{

    "url":    "https://yourapp.example/webhooks/dzbuild",

    "events": ["order.created", "order.confirmed", "order.shipped",

               "order.cancelled", "signup.counted"]

  }'
```

### Réponse 200[​](#réponse-200 "Lien direct vers Réponse 200")

```
{

  "data": {

    "id":     17,

    "secret": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",

    "note":   "Save the secret now — it is not retrievable after this response."

  },

  "meta": { "request_id": "...", "api_version": "v1" }

}
```

Ne branchez jamais sur `== 201`

Les endpoints de création `/v1/webhooks`, `/v1/products`, `/v1/orders`, `/v1/landing-pages` et `/v1/keys` répondent **200** en cas de succès, tandis que d'autres, comme `/v1/categories`, répondent **201**. Branchez plutôt sur n'importe quel 2xx.

**Le `secret` est affiché UNE FOIS.** Stockez-le à côté de l'id du webhook dans votre coffre : chaque livraison vers ce webhook est signée avec lui (voir [Signature](https://dzbuild.com/fr/fr/api-docs/webhooks/overview.md#signature)). Si vous le perdez, supprimez le webhook et créez-en un nouveau.

### Erreurs[​](#erreurs "Lien direct vers Erreurs")

| HTTP | Code / Message                                                          | Cause                                                                                                             |
| ---- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request` "url must be a valid http(s) URL"                         | URL mal formée                                                                                                    |
| 400  | `bad_request` "url must be https"                                       | Une URL en `http://`                                                                                              |
| 400  | `bad_request` "url host must be a hostname, not an IP literal"          | Une adresse IP au lieu d'un nom d'hôte                                                                            |
| 400  | `bad_request` "url host is missing or carries credentials"              | Une partie `user:password@` dans l'URL                                                                            |
| 400  | `bad_request` "url host must be a public hostname"                      | Un hôte qui n'est pas un nom de domaine complet, comme `localhost`                                                |
| 400  | `bad_request` "url host does not resolve"                               | Le nom d'hôte n'a aucun enregistrement A ou AAAA                                                                  |
| 400  | `bad_request` "url must resolve to a public address"                    | L'hôte pointe vers au moins une adresse privée ou réservée                                                        |
| 400  | `bad_request` "url port must be 80 or 443"                              | Un port personnalisé comme `:8443`                                                                                |
| 400  | `bad_request` "Body must be valid JSON"                                 | Le body de la requête n'est pas du JSON valide                                                                    |
| 400  | `bad_request` "events must be a non-empty list"                         | Tableau vide                                                                                                      |
| 400  | `bad_request` "unknown event: foo. Allowed: …"                          | Nom d'event hors catalogue                                                                                        |
| 400  | `bad_request` "Idempotency-Key header is required for write requests"   | `Idempotency-Key` manquant sur un POST/DELETE                                                                     |
| 400  | `bad_request` "Idempotency-Key must be `<=64` chars, `[A-Za-z0-9_-:.]`" | Clé trop longue, ou contenant des caractères hors de cet ensemble (les `+`, `/`, `=` du base64 sont tous rejetés) |
| 403  | `forbidden` "API is in pilot mode; key not enrolled"                    | La clé existe mais n'est pas inscrite au pilote                                                                   |
| 403  | `forbidden` "API access requires an active Enterprise plan"             | La boutique n'a pas de plan Enterprise actif                                                                      |
| 403  | `forbidden` "Apps cannot use this endpoint"                             | Le jeton appartient à une application installée                                                                   |
| 403  | `forbidden` "Missing scope: webhooks<!-- -->:write<!-- -->"             | La clé n'a pas le scope `webhooks:write`                                                                          |
| 500  | `server_error` "Could not register webhook"                             | Généralement une `url` de plus de 500 caractères                                                                  |

## `GET /v1/webhooks` — liste[​](#get-v1webhooks--liste "Lien direct vers get-v1webhooks--liste")

**Auth :** clé plateforme avec `webhooks:read`.

```
curl https://api.dzbuild.app/v1/webhooks \

  -H "Authorization: Bearer $DZ_KEY"
```

```
{

  "data": {

    "items": [

      {

        "id":              17,

        "url":             "https://yourapp.example/webhooks/dzbuild",

        "events":          ["order.created", "order.confirmed"],

        "status":          "active",

        "last_success_at": "2026-04-30 21:18:23",

        "last_failure_at": null,

        "failure_count":   0,

        "created_at":      "2026-04-30 19:00:00"

      }

    ],

    "allowed_events": [

      "order.created", "order.confirmed", "order.shipped", "order.delivered",

      "order.cancelled", "order.returned", "payment.received",

      "signup.counted", "event.recorded", "product.stock_low"

    ]

  }

}
```

`allowed_events` est la liste que l'API accepte à l'enregistrement — mais elle est plus large que ce qui se déclenche réellement. `payment.received`, `event.recorded` et `product.stock_low` sont acceptés puis jamais émis. Vérifiez le [Catalogue d'événements](https://dzbuild.com/fr/fr/api-docs/webhooks/events.md) avant de construire sur l'un d'eux.

| Statut   | Signification                                                                                                                                                                                         |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `active` | Reçoit les livraisons. En pratique, c'est la seule valeur que vous verrez.                                                                                                                            |
| `paused` | Réservé pour un usage futur, non utilisé actuellement — et il n'existe pas de page dashboard pour les webhooks API v1.                                                                                |
| `dead`   | Réservé pour un usage futur, non utilisé actuellement. Il n'y a pas de désactivation automatique ; `failure_count` se contente de compter les tentatives échouées et retombe à `0` au succès suivant. |

Pour arrêter les livraisons, supprimez le webhook.

## `POST /v1/webhooks/{id}/test`[​](#post-v1webhooksidtest "Lien direct vers post-v1webhooksidtest")

Déclenche une livraison `webhook.test` pour vérifier que votre endpoint est joignable et voir la forme de l'enveloppe.

`webhook.test` ne peut pas faire l'objet d'un abonnement : le mettre dans le tableau `events` à l'enregistrement renvoie `400 bad_request` `"unknown event: webhook.test. Allowed: …"`. Une livraison de test est envoyée au webhook ciblé quels que soient les événements auxquels il est abonné.

**Auth :** clé plateforme avec `webhooks:write`. **Nécessite `Idempotency-Key`.**

```
curl -X POST 'https://api.dzbuild.app/v1/webhooks/17/test' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: test-17-$(date +%s)"
```

```
{ "data": { "tested": true, "note": "a webhook.test delivery was enqueued; check your endpoint" } }
```

La livraison de test ressemble à :

```
{

  "event":       "webhook.test",

  "store_id":    13,

  "occurred_at": "2026-04-30T21:24:17+00:00",

  "data":        { "ts": 1717112657 },

  "delivery_id": "9f2c41ab77e05d18"

}
```

Deux choses à savoir :

* **Elle est mise en file, pas synchrone.** La réponse confirme seulement que la livraison a été enfilée ; le POST arrive peu après, généralement dans la minute.
* **Elle teste votre code de vérification.** Elle est signée avec le `secret` de votre webhook comme toute autre livraison : si elle passe votre contrôle, elle prouve la joignabilité, la forme de la payload et la vérification de signature. Voir [Signature](https://dzbuild.com/fr/fr/api-docs/webhooks/overview.md#signature).

## `DELETE /v1/webhooks/{id}`[​](#delete-v1webhooksid "Lien direct vers delete-v1webhooksid")

**Auth :** clé plateforme avec `webhooks:write`. **Nécessite `Idempotency-Key`.**

```
curl -X DELETE 'https://api.dzbuild.app/v1/webhooks/17' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: del-17"
```

```
{ "data": { "deleted": true, "id": 17 } }
```

Après suppression :

* Plus aucune livraison nouvelle n'est mise en file.
* **Toutes les livraisons déjà en file pour ce webhook sont abandonnées immédiatement.** Rien n'est « tenté une dernière fois ». Si l'arriéré vous importe, suspendez vos écritures et laissez la file se vider (généralement moins d'une minute) avant de supprimer.
* Le secret du webhook est désormais inutile.

### Rejeu d'`Idempotency-Key` sur `test` et `DELETE`[​](#rejeu-didempotency-key-sur-test-et-delete "Lien direct vers rejeu-didempotency-key-sur-test-et-delete")

L'API met en cache la réponse de chaque `Idempotency-Key` pendant **24 heures** et la rejoue avec un en-tête `Idempotency-Replay: 1`. Deux conséquences :

* Réutiliser une clé littérale comme `del-17` dans les 24 heures **rejoue la réponse en cache au lieu d'exécuter un nouvel appel**. Utilisez une clé fraîche (ou une clé qui encode la tentative) dès que vous voulez vraiment que l'opération s'exécute.
* Les réponses d'erreur sont mises en cache elles aussi. Une écriture ratée rejoue son propre 4xx pendant 24 heures sous la même clé — changez de clé après avoir corrigé la requête.

Le rejeu est garanti pendant 24 heures quel que soit l'hôte appelé, et répond toujours avec `Idempotency-Replay: 1`. Préférez tout de même `https://api.dzbuild.app/v1` : le chemin `dzbuild.com/api/v1` n'est qu'un alias, et certains chemins peuvent y être bloqués.

## Exigences de l'endpoint[​](#exigences-de-lendpoint "Lien direct vers Exigences de l'endpoint")

Votre URL webhook doit :

* Répondre en **2xx** au succès. Un 5xx, un `408` ou un `429` est re-tenté 1 min, 5 min, 30 min puis 2 h après l'échec, puis abandonné ; tout autre 4xx, et tout 3xx, envoie la livraison en file morte immédiatement (voir [Comportement des retries](https://dzbuild.com/fr/fr/api-docs/webhooks/overview.md#comportement-des-retries)).
* Répondre dans les **10 secondes** au total (**5 secondes** pour la connexion). Plus lent compte comme un timeout, re-tenté comme un 5xx.
* Être en `https://` et servir un TLS valide. Les certificats sont vérifiés strictement : un certificat auto-signé échoue à chaque tentative.
* Ne pas rediriger. `Location` n'est pas suivi ; un `301`/`302` est un échec.
* Accepter `POST` avec `Content-Type: application/json`.
* Lire **le body raw** pour vérifier la signature (ne pas re-sérialiser).
* Être **idempotent** — le même `delivery_id` PEUT arriver plusieurs fois.

Piège classique dans certains frameworks : un middleware re-encode le JSON avant que votre handler le voie, et le HMAC ne match plus. Solutions :

* **Express :** `express.raw({ type: 'application/json' })` pour la route webhook, puis `JSON.parse(req.body)` dans le handler.
* **Django :** `request.body` est les bytes raw — c'est ce qu'on veut.
* **Laravel :** `$request->getContent()` retourne le body raw.
* **PHP raw :** `file_get_contents('php://input')`.

Voir [Vérifier les signatures](https://dzbuild.com/fr/fr/api-docs/webhooks/verifying-signatures.md) pour le code en 4 langages.
