Passer au contenu principal

Thèmes & vitrines personnalisés

Construisez une vitrine entièrement personnalisée en React, Vue, Next.js, Flutter ou n'importe quelle stack — alimentée par l'API DZBuild.

Écrit par Support

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

┌──────────────────────┐         ┌──────────────────────────┐
│ 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

  • 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é.

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é

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

// 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

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

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

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

// 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

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)

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 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.

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

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, 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

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)

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

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é

  • 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 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

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

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

  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

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.

Avez-vous trouvé la réponse à votre question ?