Aller au contenu principal

Produits

Le produit est l'unité vendable de base d'une boutique. Tous les appels produit sont scopés à la boutique de la clé appelante — vous ne pouvez jamais toucher accidentellement les données d'un autre marchand.

GET /v1/products

Liste les produits. Pagination par curseur. Mis en cache pendant 30 s — vérifiez l'en-tête de réponse X-Cache: HIT|MISS.

Auth : n'importe quelle clé plateforme active de la boutique. Le scope products:read est accordé par défaut et n'est pas vérifié séparément en v1 ; seul products:write est contrôlé, sur POST/PATCH/DELETE.

Paramètres de requête

ParamTypeDéfautNotes
limitint (1–200)50Taille de page
cursorstringDu next_cursor d'une réponse précédente
statusactive | draft | archivedFiltre par statut
searchstringMatch sur name (LIKE) et sku exact

Un status non reconnu est ignoré plutôt que rejeté — vous recevez la liste non filtrée, qui inclut les produits archived. Filtrez explicitement si vous ne voulez que les articles en ligne.

Requête

curl 'https://api.dzbuild.app/v1/products?limit=10&status=active' \
-H "Authorization: Bearer $DZ_KEY"

Réponse 200

{
"data": {
"items": [
{
"id": 26,
"name": "PRO",
"slug": "pro",
"short_description": null,
"price": 1000,
"compare_price": null,
"sku": "",
"stock_quantity": 0,
"track_stock": false,
"status": "active",
"has_variants": true,
"featured": false,
"primary_image": "https://cdn.dzbuild.app/uploads/products/13/13_1768313552_b33d660c_1562f6687591.webp",
"created_at": "2026-01-13 15:06:06",
"updated_at": "2026-01-13 15:12:32"
}
],
"next_cursor": null,
"has_more": false
},
"meta": { "request_id": "...", "api_version": "v1" }
}
Changement en v1.1 — les URL d'images sont désormais complètes

primary_image (ainsi que images[].url sur GET /v1/products/{id}) est maintenant une URL CDN complète, utilisable telle quelle. Avant la v1.1, les deux renvoyaient un nom de fichier nu que l'appelant devait préfixer lui-même. Si votre intégration construit ce préfixe manuellement, supprimez cette logique — la valeur commence déjà par https://.

GET /v1/products/{id}

Détail complet du produit incluant images et variantes.

Auth : n'importe quelle clé plateforme active de la boutique (products:read n'est pas vérifié séparément en v1).

Requête

curl https://api.dzbuild.app/v1/products/26 \
-H "Authorization: Bearer $DZ_KEY"

Réponse 200

{
"data": {
"id": 26,
"name": "PRO",
"slug": "pro",
"description": "- Single store\n- Up to 300 products\n- ...",
"short_description": null,
"category_id": null,
"pricing": {
"price": 1000,
"compare_price": null,
"cost_price": null
},
"inventory": {
"sku": "",
"barcode": null,
"track_stock": false,
"stock_quantity": 0,
"low_stock_alert": 5
},
"shipping": {
"weight": null, "height": null, "width": null, "length": null,
"do_insurance": false
},
"status": "active",
"featured": false,
"has_variants": true,
"images": [
{ "id": 28, "url": "https://cdn.dzbuild.app/uploads/products/13/13_1768313552_b33d660c_1562f6687591.webp",
"alt_text": "Front view", "is_primary": true, "sort_order": 0 }
],
"variants": [
{
"id": 11,
"name": "Duration",
"type": "text",
"required": true,
"sort_order": 0,
"options": [
{ "id": 14, "value": "30 days", "color_code": null, "price_adjustment": 0,
"stock": null, "sku": null, "image_id": null, "show_as_card": false,
"sort_order": 0, "is_active": true },
{ "id": 15, "value": "90 days", "color_code": null, "price_adjustment": 500,
"stock": null, "sku": null, "image_id": null, "show_as_card": false,
"sort_order": 1, "is_active": true }
]
}
],
"combinations": [],
"combination_count": 0,
"combinations_truncated": false,
"created_at": "2026-01-13 15:06:06",
"updated_at": "2026-01-13 15:12:32"
}
}
Ajouté en v1.1

images[].alt_text, les champs d'option complets (price_adjustment, sku, show_as_card, sort_order, is_active), les required / sort_order du groupe, ainsi que tout le bloc combinations sont nouveaux. combinations liste au maximum 300 entrées — combination_count donne toujours le total réel et combinations_truncated vous indique quand la liste a été tronquée.

POST /v1/products — créer

Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.

Corps

ChampTypeRequisNotes
namestring (1–255)
pricenumber ≥ 0DZD
compare_pricenumber ≥ 0 | nullPrix barré
cost_pricenumber ≥ 0 | nullInterne — jamais montré au client
descriptionstringLong format, retours à la ligne acceptés
short_descriptionstring ≤ 500Phrase courte
skustring ≤ 100SKU interne
barcodestring ≤ 100UPC/EAN
weightnumberkg, pour la livraison
shipping_height / width / lengthnumbercm
do_insuranceboolForcer l'assurance livraison sur ce produit
track_stockboolDéfaut false
stock_quantityint ≥ 0Si track_stock
low_stock_alertint ≥ 0Défaut 5. Alimente le badge « stock bas » du tableau de bord.
variant_stock_enabledboolSuivi du stock par option de variante (Rouge, L, …)
combination_stock_enabledboolSuivi du stock par combinaison de variantes (Rouge+L). Implique variant_stock_enabled.
category_idintDoit exister dans votre boutique
statusactive | draft | archivedDéfaut draft
featuredboolDéfaut false

Quand variant_stock_enabled ou combination_stock_enabled vaut true, track_stock est désactivé automatiquement (les variantes gèrent leur propre stock).

Vous avez rarement besoin de ces deux drapeaux directement : PUT /v1/products/{id}/variants les positionne pour vous en fonction de la charge utile envoyée (stock par option ou combinaisons).

Limite par plan

Free : 5 produits actifs. Pro : 300. Unlimited / Enterprise : illimité. Le contrôle ne compte que les produits en status: "active" — les brouillons ne comptent pas — et le comptage est toujours effectué en direct au moment de l'appel. Il ne s'exécute qu'à la création : faire passer un brouillon existant à active via PATCH n'est jamais bloqué, donc une boutique en plan Free peut dépasser 5 produits actifs par ce biais. Un nom de plan non reconnu retombe sur la limite Free de 5. À la limite :

{ "error": { "code": "bad_request",
"message": "Plan 'free' allows at most 5 active products. Upgrade to add more." } }

Requête

curl -X POST 'https://api.dzbuild.app/v1/products' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "T-shirt - Cotton 200gsm",
"price": 1500,
"compare_price": 1900,
"description": "100% cotton, made in Algeria.",
"sku": "TS-COT-200",
"stock_quantity": 50,
"track_stock": true,
"status": "draft"
}'

Réponse 200

Une création réussie renvoie HTTP 200 (et non 201) avec le même corps que GET /v1/products/{id}. Ne testez pas status === 201 — vérifiez data.id à la place. id, slug et created_at sont maintenant remplis.

À la création, le slug est toujours dérivé de name — un slug envoyé dans le corps est ignoré. Pour fixer un slug précis, créez d'abord, puis PATCH /v1/products/{id} avec {"slug":"…"}. La normalisation passe en minuscules et remplace chaque suite de caractères non alphanumériques par - (compatible Unicode — les lettres arabes et accentuées sont conservées, ce n'est donc PAS [a-z0-9-]), avec troncature à 200 caractères ; les collisions reçoivent les suffixes -2, -3, …

Erreurs

CodeCause
bad_request "Body must be valid JSON"Content-Type incorrect ou JSON malformé
bad_request "name is required (1-255 chars)"Nom manquant ou trop long
bad_request "price must be a non-negative number"Prix invalide
bad_request "category_id N does not belong to this store"ID de catégorie cross-store
bad_request "Plan 'free' allows at most …"Limite de plan

PATCH /v1/products/{id} — mettre à jour

Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.

Mise à jour partielle — n'envoyez que les champs à changer. Les champs non spécifiés sont préservés.

curl -X PATCH 'https://api.dzbuild.app/v1/products/26' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "price": 1200, "status": "active" }'

Renvoie 200 et le produit complet mis à jour. Si le produit n'existe pas (ou appartient à une autre boutique), vous obtenez 404 not_found.

Renommer via PATCH { name: ... } régénère automatiquement le slug uniquement si vous n'avez pas envoyé slug explicitement. Envoyez slug pour préserver une URL spécifique après un renommage.

DELETE /v1/products/{id}

Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.

curl -X DELETE 'https://api.dzbuild.app/v1/products/26' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: del-26-2026-04-30"

Réponse :

{ "data": { "deleted": true, "id": 26 } }

C'est une suppression dure — le produit est supprimé avec ses images, variantes, offres, add-ons et combinaisons. Les fichiers images stockés sont nettoyés séparément peu après : l'appel API n'attend pas cette suppression.

La suppression détache l'historique et casse les landing pages liées

Les anciennes commandes conservent leurs lignes, et le nom du produit / sku / prix capturés à l'achat restent intacts, donc elles se lisent toujours correctement — mais la ligne n'est plus reliée à un produit (product_id devient null). Toute landing page pointant vers le produit voit son product_id effacé, ce qui casse le formulaire de commande de cette page (une landing page sans product id est une cause connue de commandes mal tarifées). Préférez PATCH { "status": "archived" } à la suppression.

POST /v1/products/{id}/images — ajouter une image

Ajouté en v1.1. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.

Vous fournissez une URL https publique ; DZBuild télécharge l'image côté serveur, la convertit, l'optimise et l'héberge sur le CDN de la boutique. Il n'y a pas d'upload de fichier via l'API — donnez le lien de l'image et nous allons la chercher.

Corps

ChampTypeRequisNotes
urlstring ≤ 2000Lien https:// public vers le fichier image
alt_textstring ≤ 255Texte d'accessibilité / SEO
is_primaryboolFaire de cette image la photo principale du produit
curl -X POST 'https://api.dzbuild.app/v1/products/26/images' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "url": "https://example.com/tshirt-front.jpg", "alt_text": "T-shirt front" }'
{ "data": { "image": { "id": 88,
"url": "https://cdn.dzbuild.app/uploads/products/13/13_1786570549_77c4_4d0c.webp",
"alt_text": "T-shirt front", "is_primary": true, "sort_order": 0,
"file_size": 27652, "width": 1000, "height": 1000 },
"deduplicated": false } }

Règles à connaître :

  • La première image d'un produit devient automatiquement l'image principale.
  • Envoyer une URL dont les octets sont déjà attachés au produit ne crée pas de doublon — vous récupérez l'image existante avec "deduplicated": true (HTTP 200 au lieu de 201).
  • Formats acceptés : JPEG, PNG, WebP, GIF, BMP, AVIF, HEIC/HEIF, TIFF. Maximum 20 Mo et 10000×10000 px. Les images sont ré-encodées (EXIF supprimé) et redimensionnées pour tenir dans 2000×2000.
  • Maximum 20 images par produit.

Quelles URL sont acceptées

Pour des raisons de sécurité, le téléchargeur n'accepte que les adresses publiques et ne suit jamais les redirections. Une URL est refusée (url_refused) lorsqu'elle n'est pas en https, qu'elle porte des identifiants (https://user:pass@…), qu'elle utilise un port autre que 443, qu'elle est une adresse IP plutôt qu'un nom d'hôte, ou qu'elle résout vers une adresse privée / interne / de métadonnées cloud. Un lien qui répond par une redirection, une page de connexion ou toute autre chose qu'une image échoue avec image_fetch_failed.

Erreurs

CodeHTTPCause
url_refused422URL rejetée par les règles ci-dessus
image_fetch_failed422Hôte injoignable, redirection, réponse non-200, ou contenu qui n'est pas une image
unsupported_image422Format non supporté ou dimensions hors limites
image_too_large422Au-delà de 20 Mo
too_many_images422Le produit a déjà 20 images
not_found404Produit absent de votre boutique

PATCH /v1/products/{id}/images/{image_id}

Ajouté en v1.1. Met à jour alt_text, sort_order (0–999), ou promeut l'image avec is_primary: true.

curl -X PATCH 'https://api.dzbuild.app/v1/products/26/images/88' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "is_primary": true }'

Un produit conserve toujours exactement une image principale : is_primary: false est donc rejeté avec primary_required — promouvez une autre image à la place.

DELETE /v1/products/{id}/images/{image_id}

Ajouté en v1.1. Supprime la ligne image et les fichiers stockés associés.

{ "data": { "deleted": true, "new_primary_image_id": 89,
"variant_references_cleared": 2, "remaining_images": 3 } }

Si des options de variantes pointaient vers cette image, ces liens sont effacés (les options elles-mêmes survivent) — variant_references_cleared vous indique combien. Supprimer l'image principale promeut automatiquement la suivante.

PUT /v1/products/{id}/variants — remplacer les variantes

Ajouté en v1.1. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.

Ceci remplace TOUTES les variantes du produit

Il n'existe pas de mise à jour partielle des variantes. Lisez l'état actuel avec GET /v1/products/{id} et renvoyez tout ce que vous voulez conserver — tout ce qui est omis est supprimé. Envoyez {"groups": []} pour supprimer toutes les variantes.

Corps

ChampTypeRequisNotes
groupsarrayGroupes de variantes dans l'ordre d'affichage. [] supprime toutes les variantes.
groups[].namestring ≤ 100ex. Color, Size. Unique par produit.
groups[].typetext | color | image_text | selectable | dropdownDéfaut text. selectable = groupe d'options additionnelles à sélection multiple. dropdown = options texte affichées dans une liste déroulante.
groups[].requiredboolDéfaut true (toujours false pour selectable)
groups[].options[].namestring ≤ 100Unique à l'intérieur du groupe
groups[].options[].color_code#rrggbbPour les groupes color
groups[].options[].price_adjustmentnumberAjouté au (ou retranché du) prix de base
groups[].options[].stockint ≥ 0 | nullStock par option
groups[].options[].skustring ≤ 100SKU par option
groups[].options[].image_idintDoit être une image existante de ce produit
groups[].options[].show_as_cardboolAfficher l'option sous forme de carte image
combinationsarrayStock par combinaison (nécessite au moins 2 groupes non-selectable)
combinations[].optionsobject{ "Color": "Red", "Size": "L" } — une entrée par groupe non-selectable
combinations[].stockint ≥ 0
combinations[].skustring ≤ 100
combinations[].is_activeboolDéfaut true

Limites : 10 groupes, 100 options par groupe, 200 options au total, 1000 combinaisons.

curl -X PUT 'https://api.dzbuild.app/v1/products/26/variants' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"groups": [
{ "name": "Color", "type": "color", "options": [
{ "name": "Red", "color_code": "#ff0000", "image_id": 88 },
{ "name": "Blue", "color_code": "#0000ff" } ] },
{ "name": "Size", "type": "text", "options": [
{ "name": "L" }, { "name": "XL", "price_adjustment": 100 } ] }
],
"combinations": [
{ "options": { "Color": "Red", "Size": "L" }, "stock": 5, "sku": "TS-R-L" },
{ "options": { "Color": "Blue", "Size": "XL" }, "stock": 2 }
]
}'

Renvoie le nouveau bloc variants + combinations (même forme que GET /v1/products/{id}).

Le mode de stock est réglé pour vous

  • Combinaisons envoyées → stock par combinaison (combination_stock_enabled), track_stock au niveau produit désactivé.
  • Pas de combinaisons, mais des options portant un stock → stock par option (variant_stock_enabled), track_stock désactivé.
  • Ni l'un ni l'autre → les variantes sont purement visuelles ; le stock au niveau produit continue de fonctionner.

Erreurs

CodeHTTPCause
validation_error422Noms, types, couleurs ou nombres invalides, ou une limite dépassée
invalid_image_id422image_id n'est pas une image de ce produit
combinations_not_applicable422Combinaisons envoyées avec moins de 2 groupes non-selectable
duplicate_combination422Deux combinaisons avec le même jeu d'options
not_found404Produit absent de votre boutique

La validation s'exécute avant toute suppression — une charge utile rejetée laisse vos variantes existantes intactes.