# Thèmes & vitrines personnalisés

DZBuild livre plusieurs thèmes de vitrine prêts à l'emploi et un customizer no-code. Mais si vous voulez **un contrôle total** — votre propre front-end React/Vue/Next.js/Flutter, votre design pixel-perfect, votre routing — l'API est faite pour vous.

Ce guide construit une vitrine headless de bout en bout :

1. Rendu du catalogue (produits, catégories, variantes)
2. Construction du panier (côté client ou serveur, à votre choix)
3. Soumission de la commande via l'API
4. Réception des webhooks au changement de statut

À la fin, votre vitrine custom sera 100% interopérable avec le tableau de bord marchand DZBuild — les commandes apparaissent, le stock décrémente, la livraison s'intègre avec les transporteurs du marchand, sans compromis.

## Architecture[​](#architecture "Lien direct vers Architecture")

```
┌──────────────────────┐         ┌──────────────────────────┐

│ Vitrine custom       │  HTTPS  │  api.dzbuild.app/v1/*    │

│ (React, Vue, Flutter)│ ──────► │  Authorization: Bearer … │

│                      │         │                          │

│ - Lit les produits   │ ◄────── │  Réponses JSON           │

│ - Affiche le panier  │         │                          │

│ - Soumet les commandes│         └──────────────────────────┘

└──────────────────────┘                      │

                                              ▼

                                  Tableau de bord marchand DZBuild

                                  - Confirme les commandes

                                  - Gère le stock

                                  - S'intègre aux transporteurs
```

Vous possédez l'UI. DZBuild possède la donnée et l'opérationnel. Le marchand se connecte à `dzbuild.com/dashboard` pour gérer les commandes confirmées dans votre UI custom.

## Prérequis[​](#prérequis "Lien direct vers Prérequis")

* Une boutique DZBuild sur un plan **Enterprise** actif. L'API est une fonctionnalité Enterprise : une clé dont la boutique est sur un autre plan, ou dont l'abonnement Enterprise a expiré, reçoit `403 forbidden` `"API access requires an active Enterprise plan"` à chaque appel, et fonctionne de nouveau dès que la boutique revient sur un plan Enterprise actif.
* **Une clé API générée par le propriétaire de la boutique** dans **Paramètres → API** du tableau de bord. Les clés créées là sont déjà inscrites au pilote. Une clé non inscrite reçoit `403 forbidden` `"API is in pilot mode; key not enrolled"`.
* Un back-end (ou serverless / edge worker) qui détient la clé API. **Ne livrez jamais** le secret au navigateur — voir [Sécurité](#s%C3%A9curit%C3%A9).

Le plan fixe aussi un plafond mensuel de commandes : une **boutique en plan Free cesse d'accepter des commandes au-delà de 30 par mois calendaire**, et `POST /v1/orders` renvoie alors `400 bad_request "Monthly order limit reached for this store plan"`. Pro, Unlimited et Enterprise n'ont aucun plafond de commandes. Gérez cette erreur dans votre checkout.

## Étape 0 — obtenir une clé[​](#étape-0--obtenir-une-clé "Lien direct vers Étape 0 — obtenir une clé")

Le propriétaire de la boutique génère les clés depuis le tableau de bord marchand, dans **Paramètres → API** (`/dashboard/api`), sur un plan Enterprise actif. Une boutique détient au plus 3 clés actives qui lui sont propres, et chaque secret n'est affiché qu'une fois :

1. **Générez la première clé** dans Paramètres → API. Les clés créées là sont déjà inscrites au pilote.
2. **Créez vous-même les clés suivantes** à partir de celle-ci, ou générez-les sur la même page :

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

  -H "Authorization: Bearer $DZ_KEY" \

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

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

  -d '{"type":"platform","name":"Custom storefront — production"}'

# HTTP 200 → { "data": { "key_id", "bearer_token", "signing_secret", "note" } }
```

La nouvelle clé copie le tier de rate limit et le flag pilote de la clé appelante. **Les scopes ne sont pas sélectionnables** : une nouvelle clé plateforme reçoit l'ensemble par défaut (`store:read`, `store:write`, `products:read`, `products:write`, `orders:read`, `orders:write`, `customers:read`, `landing_pages:read`, `landing_pages:write`, `promos:read`, `promos:write`, `pixels:read`, `pixels:write`, `shipping:read`, `shipping:write`, `webhooks:read`, `webhooks:write`, `usage:read`, `analytics:read`, `whatsapp:read`, `whatsapp:send`), limité aux scopes que détient la clé appelante ; isolez donc vos environnements avec des clés séparées, pas avec des permissions plus étroites.

Copiez le `bearer_token` — affiché **une seule fois** à la création. Perdu ? Révoquez et créez-en un.

Test :

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

  -H "Authorization: Bearer $DZ_KEY"

# attendu : { "data": { "key_id": "...", "store_id": 13, "type": "platform", "scopes": [...] } }
```

## Étape 1 — rendre le catalogue[​](#étape-1--rendre-le-catalogue "Lien direct vers Étape 1 — rendre le catalogue")

```
// pages/index.js (Next.js)

export async function getServerSideProps() {

  const res = await fetch('https://api.dzbuild.app/v1/products?limit=50&status=active', {

    headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },

  });

  const { data } = await res.json();

  return { props: { products: data.items } };

}



export default function Home({ products }) {

  return (

    <ul>

      {products.map(p => (

        <li key={p.id}>

          {p.primary_image && (

            <img src={p.primary_image} alt={p.name} />

          )}

          <h2>{p.name}</h2>

          <p>{p.price} DZD</p>

          <a href={`/product/${p.slug}`}>Voir</a>

        </li>

      ))}

    </ul>

  );

}
```

Deux choses à propos des images :

* `primary_image` est une URL CDN complète (par exemple `https://cdn.dzbuild.app/uploads/products/<store_id>/<file>`). Mettez-la telle quelle dans la balise `img`, sans ajouter de préfixe.
* L'élément de liste n'inclut **pas** `store_id`. Lisez-le une fois depuis `GET /v1/store` ou `GET /v1/whoami` et gardez-le en configuration.
* `primary_image` vaut `null` quand un produit n'a pas d'image. Protégez-vous, comme ci-dessus.

Cachez la réponse de votre côté : la liste des produits est servie à neuf à chaque appel, donc les lectures répétées à grande échelle coûtent un aller-retour chacune.

## Étape 2 — page produit avec variantes[​](#étape-2--page-produit-avec-variantes "Lien direct vers Étape 2 — page produit avec variantes")

```
const res = await fetch(`https://api.dzbuild.app/v1/products/${id}`, {

  headers: { 'Authorization': `Bearer ${process.env.DZ_KEY}` },

});

const { data: product } = await res.json();
```

La réponse inclut `variants[]` — chaque entrée est un groupe de variantes avec ses options :

```
"variants": [

  { "id": 11, "name": "Color", "type": "color",

    "options": [

      { "id": 14, "value": "Red",  "color_code": "#ff0000", "image_id": 28, "stock": 12 },

      { "id": 15, "value": "Blue", "color_code": "#0000ff", "image_id": 29, "stock": 5  }

    ]

  },

  { "id": 12, "name": "Size", "type": "text",

    "options": [

      { "id": 16, "value": "S", "stock": 10 },

      { "id": 17, "value": "M", "stock": 10 },

      { "id": 18, "value": "L", "stock": 5  }

    ]

  }

]
```

Affichez un sélecteur par groupe. Pour `type: color`, des swatches avec `color_code` ; `type: text`, des labels ; `type: dropdown`, une liste déroulante ; `type: image_text`, des thumbnails : `options[].image_id` est un **id numérique** qui correspond au `images[].id` d'une entrée de la même réponse produit. Un groupe `type: selectable` est facultatif et permet au client de choisir plusieurs options. Chaque option porte aussi `price_adjustment` (le montant ajouté au prix du produit, utilisé à l'étape 3) et `is_active` ; masquez les options dont `is_active` vaut `false`.

`images[].url` est une URL CDN complète, comme `primary_image` à l'étape 1 : mettez-la telle quelle dans une balise `img`.

**Gestion stock épuisé :** `options[].stock` vaut `null` si le marchand ne suit pas le stock par variante. S'il est ≤ 0, grisez l'option. L'API ne refuse pas une commande pour une option en rupture de stock : vérifiez donc le stock dans votre UI avant le checkout. Le stock par combinaison est dans `combinations[]` de la même réponse.

## Étape 3 — construire un panier[​](#étape-3--construire-un-panier "Lien direct vers Étape 3 — construire un panier")

Le panier est **côté client** (React state, Vuex, Pinia, localStorage…). Pas besoin d'appel API pour ajouter au panier. Chaque ligne :

```
{

  product_id:   26,

  product_name: "T-shirt",

  base_price:   1500,

  quantity:     1,

  selected_variants: [

    { group_name: "Color", option_name: "Red", color_code: "#ff0000", price_adjustment: 0 },

    { group_name: "Size",  option_name: "L",   color_code: null,     price_adjustment: 200 }

  ]

}
```

Calculez le total ligne côté client : `(base_price + sum(price_adjustment)) × quantity`. Affichez le total panier au client, mais traitez-le comme de l'affichage seulement :

* **Tout prix que vous envoyez dans l'item est ignoré** (le champ de l'API est `price`). Le serveur utilise toujours le prix catalogue du `product_id`.
* **`price_adjustment` est re-résolu depuis le catalogue** via la paire `(group_name, option_name)`. En cas d'écart, la valeur catalogue l'emporte. Votre valeur n'est utilisée que lorsque la paire ne se résout pas du tout — c'est exactement ce qui arrive après qu'un marchand a renommé une option de variante, alors gardez vos chaînes `group_name`/`option_name` synchronisées avec le catalogue.
* `group_name` et `option_name` sont silencieusement tronqués à 100 caractères, et `color_code` à 7.

## Étape 4 — collecte des infos client + calcul livraison[​](#étape-4--collecte-des-infos-client--calcul-livraison "Lien direct vers Étape 4 — collecte des infos client + calcul livraison")

Formulaire checkout standard : nom, téléphone, wilaya, commune, adresse. Lisez une fois la liste des wilayas avec `GET /v1/wilayas` (noms en arabe, français et anglais) et les communes d'une wilaya avec `GET /v1/wilayas/{id}/communes`. La liste compte 58 wilayas, ou 69 sur une boutique en mode 69 wilayas, dont les commandes acceptent alors des ids de wilaya jusqu'à 69.

`GET /v1/store` est bien en service et mérite un appel au démarrage : il renvoie le nom de la boutique, son slug, sa langue, sa description, son logo, sa favicon, sa bannière, les couleurs et la police du thème, le sous-domaine, le domaine personnalisé (et s'il est vérifié), `public_url`, `hide_branding` et `created_at`. Il est servi à neuf à chaque appel.

La livraison n'est pas dans `GET /v1/store` ; elle a ses propres endpoints : `GET /v1/shipping/rates` renvoie le tarif domicile et stop desk du marchand par wilaya, `GET /v1/shipping/settings` les règles de livraison gratuite, et `GET /v1/shipping/coverage?wilaya_id=16` les stop desks du transporteur de la boutique pour cette wilaya (voir le sélecteur stop-desk plus bas). Utilisez-les pour montrer au client une estimation avant le checkout.

N'envoyez pas de prix de livraison : `POST /v1/orders` le calcule à partir des tarifs du marchand pour la wilaya et le type de livraison, applique la livraison gratuite et la surcharge de poids, et ignore tout `shipping_cost` dans le body. Si le type de livraison choisi est désactivé pour cette wilaya et l'autre activé, la commande bascule sur celui que le marchand propose. Les commandes `pickup` et `digital` ne portent aucun frais de livraison. Affichez au client `amounts.shipping_cost` et `amounts.total` de la réponse.

## Étape 5 — soumettre la commande[​](#étape-5--soumettre-la-commande "Lien direct vers Étape 5 — soumettre la commande")

```
// Sur votre back-end (Next.js API route, Edge function, server…)

async function placeOrder(req, res) {

  const cart = req.body;



  const order = {

    customer: {

      name:      cart.name,

      phone:     cart.phone,

      email:     cart.email || null,

      wilaya_id: cart.wilaya_id,

      commune:   cart.commune,

      address:   cart.address || ''

    },

    delivery: {

      type:      cart.delivery_type,           // "home" | "desk" | "pickup" | "digital"

      desk_id:   cart.desk_id || null,

      desk_name: cart.desk_name || null

    },

    items: cart.items.map(line => ({

      product_id: line.product_id,

      quantity:   line.quantity,

      variants:   line.selected_variants

    })),

    // pas de shipping_cost : le serveur calcule la livraison à partir des tarifs du marchand

    discount:       0,

    payment_method: 'cod',

    notes:          cart.notes || null

  };



  const idempKey = req.headers['x-checkout-id'] || crypto.randomUUID();



  const apiRes = await fetch('https://api.dzbuild.app/v1/orders', {

    method: 'POST',

    headers: {

      'Authorization':    `Bearer ${process.env.DZ_KEY}`,

      'Content-Type':     'application/json',

      'Idempotency-Key':  idempKey

    },

    body: JSON.stringify(order)

  });



  if (!apiRes.ok) {

    const err = await apiRes.json();

    return res.status(apiRes.status).json(err);

  }

  const { data: createdOrder } = await apiRes.json();   // HTTP 200, pas 201

  return res.json({

    order_number: createdOrder.order_number,

    total:        createdOrder.amounts.total

  });

}
```

`POST /v1/orders` répond **HTTP 200** en cas de succès, et le body est le détail canonique complet de la commande, la même forme que renvoie `GET /v1/orders/{id}`. Branchez sur `apiRes.ok`, jamais sur `status === 201`.

Le client voit son `order_number` sur la page de succès. Le dashboard marchand affiche maintenant la nouvelle commande dans la liste pending, prête à confirmer.

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

Chaque règle ci-dessous lève un `400 bad_request` avec le message en clair : exposez-les dans votre formulaire de checkout plutôt que de les découvrir en production.

| Champ                | Règle                                                                                                                                                                                     |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`              | 1 à 50 lignes, tableau non vide                                                                                                                                                           |
| `items[].product_id` | Doit appartenir à la boutique de la clé                                                                                                                                                   |
| `items[].quantity`   | 1 à 9999                                                                                                                                                                                  |
| `customer.name`      | 1 à 255 caractères                                                                                                                                                                        |
| `customer.phone`     | Doit correspondre à `^\+?[0-9 ]{6,20}$` — chiffres et espaces uniquement, avec au plus un `+` en tête. **Les tirets et les parenthèses sont rejetés**, normalisez donc avant de soumettre |
| `customer.wilaya_id` | 1 à 58, ou 1 à 69 sur une boutique en mode 69 wilayas                                                                                                                                     |
| `customer.commune`   | 1 à 100 caractères                                                                                                                                                                        |
| `delivery.type`      | `home`, `desk`, `pickup` ou `digital` (par défaut `home`)                                                                                                                                 |
| `payment_method`     | `cod`, `free_digital` ou `digital_payment` — rien d'autre (`card`, `paypal`, … sont rejetés)                                                                                              |
| `discount`           | Doit être `>= 0`. Plafonné au sous-total plus la livraison (pas une erreur). Tout `shipping_cost` ou `payment_fee` envoyé est ignoré                                                      |
| `notes`              | Tronqué à 1000 caractères (pas une erreur)                                                                                                                                                |

Plus le plafond de plan : sur une boutique en plan Free, la 31e commande d'un mois calendaire renvoie `400 bad_request "Monthly order limit reached for this store plan"`.

## Étape 6 — recevoir les webhooks (optionnel mais puissant)[​](#étape-6--recevoir-les-webhooks-optionnel-mais-puissant "Lien direct vers Étape 6 — recevoir les webhooks (optionnel mais puissant)")

Enregistrez un webhook pour réagir aux events de commande :

```
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://yourstorefront.com/api/dz-webhook",

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

  }'
```

Vous recevrez un `secret` dans la réponse (HTTP 200) : stockez-le. Chaque livraison est signée avec lui : vérifiez `X-DZ-Signature` avant d'agir sur une livraison. Voir [Vérifier les signatures](https://dzbuild.com/fr/fr/api-docs/webhooks/verifying-signatures.md) pour la recette et le code.

Deux limites à connaître avant de construire là-dessus : en API v1, `order.created` se déclenche **uniquement** pour les commandes que votre vitrine crée via l'API (pas pour celles passées sur la vitrine DZBuild du marchand), et les événements de statut ne se déclenchent que pour les changements de statut faits via l'API. Voir le [Catalogue d'événements](https://dzbuild.com/fr/fr/api-docs/webhooks/events.md).

Utilisez les webhooks pour :

* Envoyer un SMS au client à la confirmation.
* Mettre à jour CRM / Google Sheet / analytics.
* Invalider une page « merci » après confirmation marchand.
* Déclencher la livraison de produit téléchargeable après `order.delivered`.

## Patterns courants[​](#patterns-courants "Lien direct vers Patterns courants")

### Vitrine multilingue[​](#vitrine-multilingue "Lien direct vers Vitrine multilingue")

Stockez vos traductions en front. L'API renvoie les noms et descriptions tels que saisis par le marchand. Si le marchand utilise [l'add-on Multi-language](https://dzbuild.com/fr/fr/docs/addons/multi-language.md), `GET /v1/products/{id}` ne renvoie toujours que le nom et la description canoniques ; les traductions de l'add-on ne font pas partie de la réponse de l'API, donc choisissez côté client ce que veut votre client.

### Sélecteur stop-desk[​](#sélecteur-stop-desk "Lien direct vers Sélecteur stop-desk")

Pour `delivery.type = "desk"` :

```
// 1. Lisez les stop desks du transporteur de la boutique pour la wilaya choisie :

//    GET /v1/shipping/coverage?wilaya_id=16  (scope shipping:read)

//    desks[] contient desk_id, name, address, phone, commune_id ;

//    422 no_courier_linked signifie qu'aucun transporteur n'est lié à la boutique



// 2. Affichez la liste, le client choisit :

selectedDesk = { id: 7842, name: "Yalidine Bab Ezzouar" };



// 3. Passez à la commande :

order.delivery = {

  type:      "desk",

  desk_id:    selectedDesk.id,

  desk_name:  selectedDesk.name

};
```

### Paiement en ligne (SlickPay / Edahabia)[​](#paiement-en-ligne-slickpay--edahabia "Lien direct vers Paiement en ligne (SlickPay / Edahabia)")

Pour `payment_method = "digital_payment"` :

1. Soumettez la commande via l'API normalement ; créée en `pending` avec `payment_status = pending`.
2. Redirigez le client vers SlickPay / Edahabia checkout.
3. Au succès, votre back-end reçoit la confirmation du prestataire de paiement. Vous pouvez alors faire avancer la commande avec `PATCH /v1/orders/{id}` et `{"status": "confirmed"}`. `payment_status` ne peut pas être modifié via l'API, et l'intégration SlickPay de la boutique ne marque payées que les commandes réglées sur le checkout de la boutique elle-même : elle ne marque donc pas payée une commande créée via l'API.

### Retours & remboursements[​](#retours--remboursements "Lien direct vers Retours & remboursements")

Gérés actuellement dans le dashboard. Via l'API, vous pouvez marquer une commande comme retournée avec `PATCH /v1/orders/{id}` et `{"status": "returned"}`, depuis `shipped` ou `delivered`. Aucun endpoint de remboursement n'est disponible.

## Sécurité[​](#sécurité "Lien direct vers Sécurité")

* **Ne mettez jamais** la clé API dans du code exposé au navigateur. Les clés appartiennent à votre serveur / serverless / edge. Le navigateur appelle **votre** endpoint, votre endpoint appelle DZBuild.
* **HTTPS only** entre vitrine et `api.dzbuild.app`. Le HTTP simple est rejeté.
* **Idempotency-Key requis** sur chaque `POST`, `PATCH` et `DELETE` (facultatif sur `PUT`). Sans lui vous obtenez un `400` avec le code `bad_request` et le message `"Idempotency-Key header is required for write requests"` : le code est `bad_request`, pas `idempotency_key_required`. La valeur doit faire ≤ 64 caractères pris dans `[A-Za-z0-9_-:.]` ; `crypto.randomUUID()` passe, mais pas le base64 ni la plupart des encodages de hash (`+`, `/`, `=` sont rejetés).
* **Les limites de taux** sont comptées par **boutique** : toutes les clés de la boutique partagent un même budget de 600 requêtes par minute, donc des clés supplémentaires ne l'augmentent pas. Seul le plan Enterprise a accès à l'API, et une boutique qui le quitte reçoit `403` jusqu'à son retour sur un plan Enterprise actif. Notez que ces limites ne s'appliquent qu'aux appels API ; la vitrine que vous construisez sert vos acheteurs sans les consommer. Voir [Limites de taux](https://dzbuild.com/fr/fr/api-docs/rate-limits.md) pour les plafonds actuels.
* **Une clé par environnement.** Pas de partage dev/prod. Créez une clé staging séparée ; révoquez-la après staging. (Vous ne pouvez pas choisir les scopes, et une clé garde ceux reçus à sa création : la séparation vient de la clé elle-même.)

## Dépannage[​](#dépannage "Lien direct vers Dépannage")

| Problème                                                            | Cause probable                                                                                         | Fix                                                                               |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `401 unauthorized "Missing Authorization header"`                   | En-tête `Authorization: Bearer …` oublié                                                               | Ajoutez                                                                           |
| `401 unauthorized "Invalid bearer format"`                          | Le token n'a pas de point — souvent un copier-coller tronqué qui n'a gardé que le key id `dzpk_live_…` | Utilisez le bearer token complet de 73 caractères                                 |
| `401 unauthorized "Invalid or revoked API key"`                     | Typo, clé révoquée ou expirée, ou mauvais env var                                                      | Générez une nouvelle clé dans **Paramètres → API**                                |
| `403 forbidden "API is in pilot mode; key not enrolled"`            | La clé existe mais DZBuild ne l'a pas inscrite au pilote                                               | Contactez le support                                                              |
| `403 forbidden "API access requires an active Enterprise plan"`     | La boutique n'est pas en Enterprise, ou son abonnement Enterprise a expiré                             | Le marchand renouvelle ou passe en Enterprise ; la même clé fonctionne de nouveau |
| `400 bad_request "Product N does not belong to this store"`         | ID cross-store (clé store A, produit store B)                                                          | Bonne clé                                                                         |
| `400 bad_request "items must be a non-empty array"`                 | Panier vide                                                                                            | Ne soumettez pas vide                                                             |
| `400 bad_request "Monthly order limit reached for this store plan"` | Boutique en plan Free au plafond de 30 commandes/mois                                                  | Le marchand passe en Pro ou au-dessus                                             |
| `429 rate_limited`                                                  | Polling trop agressif                                                                                  | Passez aux webhooks ; temporisez avec l'en-tête `Retry-After`                     |

## Exemples de code[​](#exemples-de-code "Lien direct vers Exemples de code")

Tout ce dont vous avez besoin est sur cette page — liste du catalogue, rendu des variantes, forme du panier, soumission de commande, enregistrement de webhook. Copiez depuis les sections ci-dessus ; il n'existe pas de dépôts de démarrage séparés à cloner.

## Mise en production[​](#mise-en-production "Lien direct vers Mise en production")

1. Testez à fond avec une clé `pilot` sur un store de test.
2. Créez une clé séparée prod (mêmes scopes, autre nom).
3. Déployez avec la clé prod en env vars.
4. Passez une vraie commande test ; confirmez sur dashboard.
5. Testez un remboursement si vous en offrez.
6. Enregistrez vos webhooks pointant sur l'URL prod.
7. Surveillez `GET /v1/usage` chaque jour la première semaine.

## Roadmap[​](#roadmap "Lien direct vers Roadmap")

| Feature                                      | Statut                                                                                     |
| -------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Tarifs de livraison par wilaya et stop desks | En service : `GET /v1/shipping/rates` et `GET /v1/shipping/coverage?wilaya_id=`            |
| Stock par combinaison                        | En service : `combinations[]` sur `GET /v1/products/{id}` et `GET /v1/products/{id}/stock` |
| `POST /v1/orders/{id}/refund` via API        | Non disponible                                                                             |
| Upload d'image produit                       | En service : `POST /v1/products/{id}/images` accepte une URL d'image https publique        |
| Champs multilingues sur réponses produits    | Non disponible                                                                             |

Besoin d'une de ces features plus tôt ? [Contactez le support](https://dzbuild.com/fr/fr/docs/operations/faq.md).
