# Codes promo

Ces quatre endpoints gèrent les codes promo de la boutique, les codes que l'acheteur saisit au moment de commander pour payer moins. Ils travaillent sur les mêmes codes que la page des codes promo du dashboard, décrite dans [Codes de réduction](https://dzbuild.com/fr/fr/docs/selling/discount-codes.md), et ils peuvent aussi renommer un code, ce que le dashboard ne permet pas.

La réduction d'un code s'applique au sous-total des produits, jamais à la livraison. Un code `percentage` prend ce pourcentage du sous-total. Un code `fixed` soustrait son montant en DZD et ne retire jamais plus que le sous-total. Un code ne peut pas être limité à un produit, une catégorie ou un client, ne peut pas plafonner la réduction sur un gros panier et ne peut pas offrir la livraison gratuite.

## Avant de commencer[​](#avant-de-commencer "Lien direct vers Avant de commencer")

* L'add-on **Codes promotionnels** doit être activé sur la boutique (`/dashboard/addons`). La liste fonctionne sans lui et l'indique dans `addon_enabled`. Toute écriture répond `409 addon_inactive` tant que l'add-on est inactif, et les acheteurs ne peuvent utiliser aucun code au moment de commander pendant ce temps.
* La clé a besoin des portées des codes promo. Les clés créées depuis le dashboard (`/dashboard/api`) ont les deux. Les portées sont figées à la création de la clé : une clé qui ne les a pas répond `403 forbidden`. Créez une nouvelle clé depuis le dashboard.
* `starts_at` et `expires_at` sont à l'heure de l'Algérie, au format `YYYY-MM-DD HH:MM:SS`.

| Portée         | Description                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------- |
| `promos:read`  | Consulter les codes promo.                                                                  |
| `promos:write` | Créer, modifier et supprimer des codes promo. Cela change les prix payés par vos acheteurs. |

## L'objet code promo[​](#lobjet-code-promo "Lien direct vers L'objet code promo")

| Champ                      | Type           | Signification                                                                                                      |
| -------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `id`                       | int            | L'id du code dans la boutique.                                                                                     |
| `code`                     | string         | Ce que l'acheteur saisit. En majuscules, `A-Z`, `0-9`, `-` et `_`, unique dans la boutique.                        |
| `discount_type`            | string         | `percentage` ou `fixed`.                                                                                           |
| `discount_value`           | number         | Le pourcentage (supérieur à 0, au plus 100) ou le montant en DZD.                                                  |
| `min_order_amount`         | number ou null | Le sous-total produits qu'une commande doit atteindre pour que le code s'applique. `null` veut dire aucun minimum. |
| `max_uses`                 | int ou null    | Combien de commandes peuvent utiliser le code. `null` veut dire illimité.                                          |
| `used_count`               | int            | Combien de commandes l'ont utilisé. En lecture seule.                                                              |
| `is_active`                | bool           | Un code inactif est refusé au moment de commander.                                                                 |
| `starts_at`                | string ou null | Avant cette date, le code est refusé. `null` veut dire qu'il fonctionne tout de suite.                             |
| `expires_at`               | string ou null | Après cette date, le code est refusé. `null` veut dire qu'il n'expire jamais.                                      |
| `created_at`, `updated_at` | string         | `YYYY-MM-DD HH:MM:SS`, heure du serveur.                                                                           |

## `GET /v1/promo-codes`[​](#get-v1promo-codes "Lien direct vers get-v1promo-codes")

Les codes de la boutique, du plus récent au plus ancien. Aucun appel ne lit un seul code par son id : parcourez cette liste.

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

### Paramètres de requête[​](#paramètres-de-requête "Lien direct vers Paramètres de requête")

| Param       | Type   | Défaut | Notes                                                                                                                                     |
| ----------- | ------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `is_active` | string | aucun  | `true`, `1`, `yes` ou `on` renvoie les codes actifs. Toute autre valeur renvoie les codes inactifs. Omettez-le pour avoir tous les codes. |
| `limit`     | int    | 50     | De 1 à 200.                                                                                                                               |
| `cursor`    | string | aucun  | Le `next_cursor` de la page précédente. Voir [Pagination](https://dzbuild.com/fr/fr/api-docs/pagination.md).                              |

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

```
curl 'https://api.dzbuild.app/v1/promo-codes?is_active=true' \

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

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

```
{

  "data": {

    "items": [

      {

        "id": 20,

        "code": "WELCOME10",

        "discount_type": "percentage",

        "discount_value": 10,

        "min_order_amount": 3000,

        "max_uses": 100,

        "used_count": 7,

        "is_active": true,

        "starts_at": null,

        "expires_at": "2026-11-30 23:59:00",

        "created_at": "2026-10-06 14:20:11",

        "updated_at": "2026-10-06 14:20:11"

      }

    ],

    "next_cursor": null,

    "has_more": false,

    "addon_enabled": true

  }

}
```

`addon_enabled` indique si l'add-on **Codes promotionnels** est activé. Quand il vaut `false`, la liste répond toujours, mais les écritures échouent et les acheteurs ne peuvent pas utiliser les codes.

## `POST /v1/promo-codes`[​](#post-v1promo-codes "Lien direct vers post-v1promo-codes")

Crée un code. Il fonctionne au moment de commander dès sa création, sauf si vous envoyez `is_active: false` ou un `starts_at` dans le futur.

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

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

| Champ                   | Type           | Requis | Notes                                                                                                                                                                                                                                                    |
| ----------------------- | -------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`                  | string         | oui    | Les espaces autour sont retirés et les lettres passent en majuscules, puis il doit faire de 2 à 30 caractères parmi `A-Z`, `0-9`, `-` ou `_`. Un code qui existe déjà dans la boutique répond `409 code_exists`.                                         |
| `discount_type`         | string         | oui    | `percentage` ou `fixed`, en majuscules ou en minuscules. Il n'y a pas de valeur par défaut.                                                                                                                                                              |
| `discount_value`        | number         | oui    | Supérieur à 0. Au plus 100 pour `percentage`, et 100 demande la confirmation du marchand (voir la section sur la réduction de 100% plus bas). Aucune limite haute pour `fixed`.                                                                          |
| `min_order_amount`      | number ou null | non    | Sous-total produits minimum en DZD. `0`, `""` ou `null` veut dire aucun minimum.                                                                                                                                                                         |
| `max_uses`              | int ou null    | non    | 1 ou plus. `0`, `""` ou `null` veut dire illimité.                                                                                                                                                                                                       |
| `is_active`             | bool           | non    | `true` par défaut. Envoyez un booléen JSON : une chaîne comme `"false"` est lue comme `true`.                                                                                                                                                            |
| `starts_at`             | string ou null | non    | Un format courant de date et d'heure, comme `2026-11-01 08:00` ou une chaîne ISO 8601. Enregistré au format `YYYY-MM-DD HH:MM:SS` à l'heure de l'Algérie ; une valeur avec un décalage UTC est convertie. `null` ou `""` veut dire aucune date de début. |
| `expires_at`            | string ou null | non    | Mêmes formats. Doit être dans le futur et après `starts_at`. `null` ou `""` veut dire aucune expiration.                                                                                                                                                 |
| `confirm_token`         | string         | non    | Seulement pour une réduction de 100%.                                                                                                                                                                                                                    |
| `confirm_full_discount` | bool           | non    | Seulement pour une réduction de 100%.                                                                                                                                                                                                                    |

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

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

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: promo-welcome10-create" \

  -d '{"code": "welcome10", "discount_type": "percentage", "discount_value": 10, "min_order_amount": 3000, "max_uses": 100, "expires_at": "2026-11-30 23:59"}'
```

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

```
{

  "data": {

    "id": 20,

    "code": "WELCOME10",

    "discount_type": "percentage",

    "discount_value": 10,

    "min_order_amount": 3000,

    "max_uses": 100,

    "used_count": 0,

    "is_active": true,

    "starts_at": null,

    "expires_at": "2026-11-30 23:59:00",

    "created_at": "2026-10-06 14:20:11",

    "updated_at": "2026-10-06 14:20:11"

  }

}
```

## `PATCH /v1/promo-codes/{id}`[​](#patch-v1promo-codesid "Lien direct vers patch-v1promo-codesid")

Change uniquement les champs que vous envoyez, avec les mêmes règles que la création. Un champ omis garde sa valeur.

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

* `code` peut être changé. Le nouveau texte doit être libre dans la boutique, sinon l'appel répond `409 code_exists`.
* `discount_type` et `discount_value` sont vérifiés ensemble. Envoyer seulement `discount_type: "percentage"` sur un code `fixed` de 500 DZD répond `422 invalid_discount_value`, parce que 500 dépasse 100. Envoyez les deux.
* Un nouveau `expires_at` doit être dans le futur. Un code déjà expiré reste modifiable tant que vous n'envoyez pas `expires_at`.
* `used_count` ne peut pas être écrit.
* Un corps vide ne change rien et renvoie le code.
* Un code d'une autre boutique répond `404`, comme un code qui n'existe pas.

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

```
curl -X PATCH 'https://api.dzbuild.app/v1/promo-codes/20' \

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: promo-20-extend" \

  -d '{"max_uses": 200, "expires_at": "2026-12-31 23:59"}'
```

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

```
{

  "data": {

    "id": 20,

    "code": "WELCOME10",

    "discount_type": "percentage",

    "discount_value": 10,

    "min_order_amount": 3000,

    "max_uses": 200,

    "used_count": 7,

    "is_active": true,

    "starts_at": null,

    "expires_at": "2026-12-31 23:59:00",

    "created_at": "2026-10-06 14:20:11",

    "updated_at": "2026-10-20 09:05:42"

  }

}
```

## `DELETE /v1/promo-codes/{id}`[​](#delete-v1promo-codesid "Lien direct vers delete-v1promo-codesid")

Supprime le code : les acheteurs ne peuvent plus l'utiliser. Les commandes déjà passées avec lui gardent leur réduction. Pour mettre un code en pause sans perdre son nombre d'utilisations, envoyez plutôt `is_active: false` avec `PATCH`.

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

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

```
curl -X DELETE 'https://api.dzbuild.app/v1/promo-codes/20' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: promo-20-delete"
```

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

```
{

  "data": {

    "deleted": true,

    "id": 20

  }

}
```

## Réduction de 100%[​](#réduction-de-100 "Lien direct vers Réduction de 100%")

Un code `percentage` à 100 rend les produits gratuits pour tout acheteur qui a le code. Une création, ou une modification qui envoie `discount_type` ou `discount_value`, n'est pas écrite au premier appel quand le résultat est un code `percentage` à 100% :

1. L'appel répond `422 confirmation_required` et n'écrit rien. L'erreur porte `confirm_token` (usage unique, valable `600` secondes), `action` et `will_change`, un résumé à montrer au marchand.
2. Une fois que le marchand approuve, renvoyez le même corps avec `confirm_token` en plus et une **nouvelle** `Idempotency-Key` (la première clé est liée au corps sans le jeton). Un jeton expiré, déjà utilisé ou qui ne correspond plus à la requête répond `422 confirmation_stale` avec un nouveau jeton.

Avec une clé créée depuis le dashboard, vous pouvez éviter cet aller-retour en envoyant `confirm_full_discount: true` dès le premier appel. Le DZBuild Copilot ne peut pas utiliser ce champ et passe toujours par le jeton.

```
{

  "error": {

    "code": "confirmation_required",

    "message": "A 100% discount makes every order free ...",

    "confirm_token": "cft_xxxxxxxxxxxxxxxx",

    "confirm_token_expires_in": 600,

    "action": "promo.full_discount:new",

    "will_change": {

      "action": "Create a promo code that makes orders free",

      "code": "FREEGIFT",

      "discount": "100% off the whole order subtotal",

      "reversible": true,

      "note": "Any customer with this code pays 0 for the goods. Orders already placed with it cannot be reversed by deleting the code."

    }

  }

}
```

Sur une modification, `action` se termine par l'id du code au lieu de `new`, et `will_change.action` devient `Change promo code #20 to make orders free`.

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

  -H "Authorization: Bearer $DZ_KEY" \

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

  -H "Idempotency-Key: promo-freegift-approved" \

  -d '{"code": "FREEGIFT", "discount_type": "percentage", "discount_value": 100, "max_uses": 1, "confirm_token": "cft_REPLACE_WITH_TOKEN"}'
```

## Historique des changements et annulation[​](#historique-des-changements-et-annulation "Lien direct vers Historique des changements et annulation")

Chaque création, modification et suppression faite par l'API est enregistrée. Les codes modifiés sur la page du dashboard ne sont pas enregistrés, on ne peut donc pas les annuler par l'API.

* `GET /v1/changes?entity=promo_code` liste les changements des codes promo, du plus récent au plus ancien, avec `store:read`. Chaque élément porte l'`id` du changement, l'id du code dans `entity_id`, l'`action` (`create`, `update` ou `delete`) et un `summary`.
* `POST /v1/changes/{id}/undo` demande `promos:write`, une `Idempotency-Key` et l'add-on activé, comme toute écriture.
* Annuler une modification remet les valeurs précédentes, sans confirmation, même une réduction de 100% ou une date d'expiration déjà passée. Si le code a été supprimé depuis, l'annulation répond `422 restore_target_missing`.
* Annuler une suppression recrée le code avec son nombre d'utilisations, et garde son id quand cet id est encore libre.
* Annuler une création répond `422 nothing_to_restore` : supprimez plutôt le code.
* Le jeton d'une application installée ne peut ni lire ni annuler les changements : les deux répondent `403 forbidden`.

```
curl -X POST 'https://api.dzbuild.app/v1/changes/500/undo' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: undo-500"
```

```
{

  "data": {

    "undone": true,

    "change_id": 500,

    "entity": "promo_code",

    "undo_change_id": 510

  }

}
```

Un changement annulé une seconde fois répond `409 already_undone`.

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

| HTTP | Code                                          | Cause                                                                                                                                                                                         |
| ---- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`                                 | L'id dans le chemin n'est pas composé de chiffres, le corps n'est pas un JSON valide, ou `Idempotency-Key` manque ou est mal formé.                                                           |
| 403  | `forbidden`                                   | `Missing scope: promos:read` ou `Missing scope: promos:write`, ou `API access requires an active Enterprise plan` pour une clé du marchand dont la boutique n'a pas de plan Enterprise actif. |
| 404  | `not_found`                                   | Aucun code promo avec cet id dans la boutique.                                                                                                                                                |
| 409  | `addon_inactive`                              | L'add-on Codes promotionnels n'est pas activé sur la boutique. Écritures seulement.                                                                                                           |
| 409  | `code_exists`                                 | Un autre code de la boutique a déjà ce texte.                                                                                                                                                 |
| 422  | `invalid_code`                                | `code` fait moins de 2 ou plus de 30 caractères, ou contient un caractère autre que `A-Z`, `0-9`, `-` et `_`.                                                                                 |
| 422  | `invalid_discount_type`                       | `discount_type` manque, ou n'est ni `percentage` ni `fixed`.                                                                                                                                  |
| 422  | `invalid_discount_value`                      | `discount_value` manque, n'est pas un nombre, vaut 0 ou moins, ou dépasse 100 pour `percentage`.                                                                                              |
| 422  | `invalid_min_order_amount`                    | `min_order_amount` n'est pas un nombre.                                                                                                                                                       |
| 422  | `invalid_max_uses`                            | `max_uses` n'est pas un nombre, ou est inférieur à 1.                                                                                                                                         |
| 422  | `invalid_starts_at`, `invalid_expires_at`     | La date ne peut pas être lue.                                                                                                                                                                 |
| 422  | `expires_at_in_past`                          | Le `expires_at` envoyé n'est pas dans le futur.                                                                                                                                               |
| 422  | `invalid_date_window`                         | `expires_at` n'est pas après `starts_at`.                                                                                                                                                     |
| 422  | `confirmation_required`, `confirmation_stale` | Une réduction de 100% attend l'accord du marchand. Voir la section plus haut.                                                                                                                 |
| 422  | `idempotency_key_reuse`                       | La même `Idempotency-Key` a servi avec un autre corps ou un autre chemin.                                                                                                                     |
| 500  | `server_error`                                | L'écriture a échoué. Réessayez avec la même `Idempotency-Key`.                                                                                                                                |

Une réponse `4xx` est conservée avec son `Idempotency-Key` pendant 24 heures et rejouée à tout réessai avec le même corps. Après avoir activé l'add-on ou corrigé le corps, envoyez l'appel avec une nouvelle clé. Les erreurs communes à tous les endpoints, comme `401`, `402` et `429`, sont dans [Erreurs](https://dzbuild.com/fr/fr/api-docs/errors.md), et les règles de réessai dans [Idempotence](https://dzbuild.com/fr/fr/api-docs/idempotency.md).

## Limites connues[​](#limites-connues "Lien direct vers Limites connues")

* **Les utilisations peuvent dépasser `max_uses`.** Quand plusieurs acheteurs commandent avec le même code au même moment, `used_count` peut finir au-dessus de `max_uses`.
* **Pas de webhook.** Créer, modifier ou supprimer un code promo n'envoie aucun événement webhook.
