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

  • Un compte marchand DZBuild. Le plan d'abonnement de la boutique n'a aucune incidence sur l'accès à l'API — passer à un plan supérieur ne l'accorde pas, et un plan expiré ne le retire pas.

  • Une clé API inscrite au pilote. C'est la vraie barrière : tant que l'API est en pilote, DZBuild doit inscrire la clé. Sans inscription, chaque appel renvoie 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é.

Un endroit où le plan du marchand compte bel et bien : 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é

Il n'existe aucune page « Developer → API Keys » dans le tableau de bord marchand. Pendant le pilote :

  1. Demandez au support DZBuild d'émettre la première clé de la boutique et de l'inscrire au pilote.

  2. Créez vous-même les clés suivantes à partir de celle-ci :

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 clé plateforme porte toujours l'ensemble par défaut complet (products:read, products:write, orders:read, orders:write, store:read, store:write, customers:read, landing_pages:read, landing_pages:write, webhooks:read, webhooks:write, usage:read) : 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={`https://cdn.dzbuild.app/${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 un chemin de stockage (généralement uploads/products/<store_id>/<file>), donc l'URL CDN est simplement https://cdn.dzbuild.app/ + ce chemin — n'insérez pas un segment /{store_id}/products/ de votre cru. Les produits plus anciens peuvent porter un simple nom de fichier ; ceux-là se résolvent sur https://cdn.dzbuild.app/uploads/products/<store_id>/<filename>.

  • 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 — la liste des produits est mise en cache pendant 30 s, donc les lectures répétées à grande échelle sont peu coûteuses.

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

Attention au nom du champ — images[].url est un chemin de stockage, pas une URL. Préfixez-le par https://cdn.dzbuild.app/ exactement comme primary_image à l'étape 1 avant de le mettre 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. Le check définitif se fait côté serveur au confirm-time.

É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. Hardcodez les 58 wilayas — c'est une liste stable.

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. C'est l'un des GET mis en cache (30 s).

Ce qu'il ne porte pas — et c'est là le vrai point v1.1 — ce sont les tarifs de livraison par wilaya et la liste des stop desks du marchand. Donc pour le coût de livraison aujourd'hui :

  • Hardcodez les tarifs par wilaya + type de livraison dans la vitrine (le plus simple), ou

  • Gardez-les dans votre propre configuration, synchronisée avec le marchand hors API.

Passez le shipping calculé à l'API de commandes en shipping_cost. Le serveur ne recalcule pas le shipping pour vous actuellement ; ce que vous passez fait partie du total de la commande.

É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,
      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
    })),
    shipping_cost:  cart.shipping_cost,
    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 (aucun endpoint de création v1 ne renvoie 201), 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

customer.commune

1 à 100 caractères

delivery.type

home, desk ou digital

payment_method

cod, free_digital ou digital_payment — rien d'autre (card, paypal, … sont rejetés)

shipping_cost, discount, payment_fee

Doivent être >= 0

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. Notez que les livraisons API v1 ne sont pas signées avec ce secret : vous ne pouvez donc pas encore les vérifier. Gardez l'URL du webhook indevinable et relisez la commande avec GET /v1/orders/{id} avant d'agir. Voir Vérifier les signatures pour les détails.

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} retournera (en v1.1) name_ar, name_fr, description_ar, description_fr à côté des champs canoniques. En attendant, seul le canonique est exposé — choisissez côté client ce que veut votre client.

Sélecteur stop-desk

Pour delivery.type = "desk" :

// 1. Lisez les stop desks du marchand pour la wilaya choisie
//    (prévu v1.1 : GET /v1/store/desks?wilaya=16)
//    En attendant, requêtez directement le transporteur (Yalidine, ZR, etc.)// 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 un webhook SlickPay → appelez PATCH /v1/orders/{id} pour passer payment_status à paid (prévu v1.1 ; en attendant le paiement est mis à jour par l'intégration SlickPay existante de la boutique).

Retours & remboursements

Gérés actuellement dans le dashboard. Le support API pour POST /v1/orders/{id}/refund est sur la roadmap.

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 toutes les écritures. 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 clé API et le plafond découle du plan actuel de la boutique — les montées et descentes de plan s'appliquent immédiatement, sans changer de clé. Rien n'est illimité à la minute. 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. (Les scopes sont identiques sur toute clé plateforme — 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 mauvais env var

Créez une nouvelle clé avec POST /v1/keys

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

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

ETA

Tarifs de livraison par wilaya + liste des stop desks sur GET /v1/store (l'endpoint lui-même est déjà en service)

v1.1

GET /v1/products/{id}/combinations pour stock par combo

v1.1

POST /v1/orders/{id}/refund via API

v1.1

Upload image produit via presigned URL

v1.1

Champs multilingues sur réponses produits

v1.1

Besoin d'une de ces features plus tôt ? Contactez le support.

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