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. Servi à neuf à chaque appel : un GET envoyé juste après une écriture renvoie les nouvelles valeurs.
Auth : clé plateforme avec products:read (scope accordé par défaut). Une clé qui ne l'a pas reçoit 403 forbidden.
Paramètres de requête
| Param | Type | Défaut | Notes |
|---|---|---|---|
limit | int (1–200) | 50 | Taille de page |
cursor | string | — | Du next_cursor d'une réponse précédente |
status | active | draft | archived | — | Filtre par statut |
search | string | — | Match 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/123/123_1700000000_example.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" }
}
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 : clé plateforme avec products:read.
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/123/123_1700000000_example.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"
}
}
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 et products:read. La réponse est le produit tel que le renvoie GET /v1/products/{id} : une clé sans products:read reçoit donc 403 forbidden alors que le produit a bien été créé. Nécessite Idempotency-Key.
Corps
| Champ | Type | Requis | Notes |
|---|---|---|---|
name | string (1–255) | ✅ | |
price | number ≥ 0 | ✅ | DZD |
compare_price | number ≥ 0 | null | Prix barré | |
cost_price | number ≥ 0 | null | Interne — jamais montré au client | |
description | string | Long format, retours à la ligne et mise en forme HTML acceptés (gras, listes, titres, liens, tableaux, images) ; les scripts et autres balises dangereuses sont retirés. Maximum 60000 octets : au-delà, la réponse est bad_request "description exceeds 60000 bytes". | |
short_description | string ≤ 500 | Phrase courte | |
sku | string ≤ 100 | SKU interne | |
barcode | string ≤ 100 | UPC/EAN | |
weight | number | kg, pour la livraison | |
shipping_height / width / length | number | cm | |
do_insurance | bool | Forcer l'assurance livraison sur ce produit | |
track_stock | bool | Défaut false | |
stock_quantity | int ≥ 0 | Si track_stock | |
low_stock_alert | int ≥ 0 | Défaut 5. Alimente le badge « stock bas » du tableau de bord. | |
variant_stock_enabled | bool | Suivi du stock par option de variante (Rouge, L, …) | |
combination_stock_enabled | bool | Suivi du stock par combinaison de variantes (Rouge+L). Implique variant_stock_enabled. | |
category_id | int | Doit exister dans votre boutique. Le produit rejoint cette catégorie, qui devient sa catégorie principale ; les catégories auxquelles il appartient déjà sont conservées. En PATCH, null efface la catégorie principale sans retirer le produit d'aucune catégorie. | |
status | active | draft | archived | Défaut draft | |
featured | bool | Dé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. Comme il s'exécute à chaque création, une boutique à sa limite ne peut pas créer de nouveau produit, même avec status: "draft". 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
| Code | Cause |
|---|---|
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 et products:read. La réponse est le produit mis à jour : une clé sans products:read reçoit donc 403 forbidden alors que la modification a bien été enregistrée. 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, combinaisons et avis clients. Les fichiers images stockés ne sont pas supprimés par cet appel : une URL d'image enregistrée auparavant peut donc continuer à s'afficher.
Les commandes passées conservent leurs lignes, et le nom, le SKU et le prix du produit capturés au moment de l'achat restent intacts, donc les anciennes commandes restent lisibles, mais la ligne ne pointe plus vers un produit (product_id devient null). Un produit utilisé par une landing page (au niveau de la page, ou dans une section formulaire de commande, bouton de commande ou offres produit) est refusé avec 409 product_in_use_by_landing_page ; les données de l'erreur listent les pages dans landing_pages[] avec id, title et slug. Supprimez d'abord cette landing page (DELETE /v1/landing-pages/{id}) ou associez-lui un autre produit (PATCH /v1/landing-pages/{id} avec un nouveau product_id), puis supprimez le produit. 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
| Champ | Type | Requis | Notes |
|---|---|---|---|
url | string ≤ 2000 | ✅ | Lien https:// public vers le fichier image |
alt_text | string ≤ 255 | Texte d'accessibilité / SEO | |
is_primary | bool | Faire 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/123/123_1700000001_example.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 converties en WebP (EXIF supprimé), et une image de plus de 2000 px de large est réduite à 2000 px de large en gardant ses proportions. Un fichier WebP de 3 Mo ou moins et d'au plus 2000 px de large est stocké tel quel.
- 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 ou un statut d'erreur échoue avec image_fetch_failed ; un lien qui répond par une page web (une page de connexion, par exemple) ou tout autre fichier qui n'est pas une image prise en charge échoue avec unsupported_image.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | url manquant ou de plus de 2000 caractères |
url_refused | 422 | URL rejetée par les règles ci-dessus |
image_fetch_failed | 422 | Hôte injoignable, redirection ou réponse non-200 |
unsupported_image | 422 | Contenu qui n'est pas une image (une page web, par exemple), format non supporté ou dimensions hors limites |
image_too_large | 422 | Au-delà de 20 Mo |
too_many_images | 422 | Le produit a déjà 20 images |
not_found | 404 | Produit 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. Idempotency-Key est facultatif : avec cet en-tête, une nouvelle tentative avec la même valeur renvoie la première réponse ; sans lui, chaque appel refait le remplacement complet.
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
| Champ | Type | Requis | Notes |
|---|---|---|---|
groups | array | ✅ | Groupes de variantes dans l'ordre d'affichage. [] supprime toutes les variantes. |
groups[].name | string ≤ 100 | ✅ | ex. Color, Size. Unique par produit. |
groups[].type | text | color | image_text | selectable | dropdown | Défaut text. selectable = groupe d'options additionnelles à sélection multiple. dropdown = options texte affichées dans une liste déroulante. | |
groups[].required | bool | Défaut true (toujours false pour selectable) | |
groups[].options[].name | string ≤ 100 | ✅ | Unique à l'intérieur du groupe |
groups[].options[].color_code | #rrggbb | Pour les groupes color | |
groups[].options[].price_adjustment | number | Ajouté au (ou retranché du) prix de base | |
groups[].options[].stock | int ≥ 0 | null | Stock par option | |
groups[].options[].sku | string ≤ 100 | SKU par option | |
groups[].options[].image_id | int | Doit être une image existante de ce produit | |
groups[].options[].show_as_card | bool | Afficher l'option sous forme de carte image | |
combinations | array | Stock par combinaison (nécessite au moins 2 groupes non-selectable) | |
combinations[].options | object | ✅ | { "Color": "Red", "Size": "L" } — une entrée par groupe non-selectable |
combinations[].stock | int ≥ 0 | ✅ | |
combinations[].sku | string ≤ 100 | ||
combinations[].is_active | bool | Dé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_stockau niveau produit désactivé. - Pas de combinaisons, mais des options portant un
stock→ stock par option (variant_stock_enabled),track_stockdésactivé. - Ni l'un ni l'autre → les variantes sont purement visuelles ; le stock au niveau produit continue de fonctionner.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | Noms, types, couleurs ou nombres invalides, ou une limite dépassée |
invalid_image_id | 422 | image_id n'est pas une image de ce produit |
combinations_not_applicable | 422 | Combinaisons envoyées avec moins de 2 groupes non-selectable |
duplicate_combination | 422 | Deux combinaisons avec le même jeu d'options |
not_found | 404 | Produit absent de votre boutique |
La validation s'exécute avant toute suppression — une charge utile rejetée laisse vos variantes existantes intactes.
GET /v1/products/{id}/stock
Lit le mode de stock du produit et la quantité actuelle de chaque cible modifiable dans ce mode. Lisez-le avant une synchronisation de stock pour obtenir les id des options et des combinaisons. Contrairement à GET /v1/products, cette lecture n'est pas mise en cache : elle montre une modification tout de suite.
Auth : clé plateforme avec products:read.
curl https://api.dzbuild.app/v1/products/30/stock \
-H "Authorization: Bearer $DZ_KEY"
{
"data": {
"product_id": 30,
"mode": "variant_options",
"track_stock": false,
"flags": { "variant_stock_enabled": true, "combination_stock_enabled": false },
"max_stock": 9999999,
"options": [
{ "target": "option", "id": 41, "group": "Size", "value": "M",
"stock": null, "unlimited": true },
{ "target": "option", "id": 42, "group": "Size", "value": "L",
"stock": 10, "unlimited": false }
]
}
}
mode indique où le stock du produit est compté, et la réponse liste les cibles de ce mode :
mode | Stock compté sur | Listé dans la réponse |
|---|---|---|
product | Le produit lui-même | product.stock_quantity |
variant_options | Chaque option de variante | options[] |
combinations | Chaque combinaison d'options | combinations[] avec sku, is_active et options (nom du groupe vers nom de l'option). options[] est aussi listé, en lecture seule. |
Une option avec "stock": null et "unlimited": true a un stock illimité. Le mode suit les variantes enregistrées avec PUT /v1/products/{id}/variants (voir plus haut).
POST /v1/products/{id}/stock
Fixe ou ajuste les quantités en stock. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.
Corps
items liste de 1 à 500 cibles. Chaque élément porte un seul de set, delta ou "unlimited": true, et une cible n'apparaît qu'une fois par requête.
| Champ | Type | Requis | Notes |
|---|---|---|---|
items[].target | product | option | combination | ✅ | Doit correspondre au mode du produit : product, option pour variant_options, combination pour combinations |
items[].id | int ≥ 1 | ✅ pour option et combination | L'id renvoyé par GET /v1/products/{id}/stock |
items[].set | int, de 0 à 9999999 | Nouvelle quantité | |
items[].delta | int, pas 0 | Unités à ajouter, négatif pour en retirer. Le résultat reste entre 0 et 9999999. | |
items[].unlimited | bool | Options uniquement. true rend le stock de l'option illimité. Une option illimitée aujourd'hui a besoin de "unlimited": false à côté de set pour commencer le décompte. |
Un set ou un delta sur la cible product active aussi track_stock : la boutique compte alors le stock de ce produit à partir de ce moment.
curl -X POST 'https://api.dzbuild.app/v1/products/30/stock' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"items": [
{ "target": "option", "id": 41, "set": 20, "unlimited": false },
{ "target": "option", "id": 42, "delta": -2 }
]
}'
Renvoie 200 avec le stock après la modification, dans la forme du GET ci-dessus : l'option 41 affiche maintenant 20 et l'option 42 affiche 8. Les éléments sont enregistrés ensemble : si l'un est refusé, aucun n'est enregistré. La modification entre dans l'historique des modifications de la boutique (GET /v1/changes), et POST /v1/changes/{id}/undo remet les quantités précédentes.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | items absent, vide ou de plus de 500 éléments ; un target, id, set ou delta invalide ; la même cible deux fois ; pas exactement un de set, delta, "unlimited": true ; "unlimited": true sur un produit ou une combinaison |
option_stock_unlimited | 422 | L'option est illimitée aujourd'hui : un set sans "unlimited": false, ou un delta quel qu'il soit |
stock_mode_mismatch | 409 | target ne correspond pas au mode de stock du produit |
not_found | 404 | Produit absent de votre boutique, ou option ou combinaison qui n'appartient pas à ce produit |
GET /v1/products/{id}/offers
Lit les offres de quantité du produit, les lots affichés sur la page produit.
Auth : clé plateforme avec products:read.
curl https://api.dzbuild.app/v1/products/26/offers \
-H "Authorization: Bearer $DZ_KEY"
{
"data": {
"product_id": 26,
"pricing_note": "price is the TOTAL for the whole bundle of `quantity` units, not a unit price.",
"offers": [
{ "id": 51, "title": "Buy 2, get 1 free", "quantity": 3, "price": 2000,
"compare_price": 3000, "discount_type": null, "discount_value": null,
"badge_text": "Best value", "badge_color": "#10b981", "free_shipping": true,
"image_path": null, "image_url": null, "sort_order": 0, "is_active": true },
{ "id": 52, "title": "Pack of 2", "quantity": 2, "price": 0,
"compare_price": null, "discount_type": "percent", "discount_value": 10,
"badge_text": null, "badge_color": "#10b981", "free_shipping": false,
"image_path": null, "image_url": null, "sort_order": 1, "is_active": true }
]
}
}
price est le total du lotprice est ce que l'acheteur paie pour toutes les unités de quantity ensemble, pas un prix unitaire. Sur un produit à 1000 DZD, « 2 achetés, le 3e offert » s'écrit "quantity": 3, "price": 2000. Une offre avec un discount_type enregistre price à 0 et retire discount_value du prix du produit multiplié par la quantité : amount retire un montant en DZD, percent retire un pourcentage et l'applique aussi aux ajustements de prix des variantes. La deuxième offre ci-dessus vend 2 unités pour 1800 DZD.
image_url est le lien CDN complet de l'image de l'offre, null si elle n'en a pas.
POST /v1/products/{id}/offers
Remplace les offres de quantité du produit. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.
Envoyez chaque offre à conserver, dans l'ordre d'affichage. Tout ce qui est omis est supprimé. Envoyez {"offers": []} pour supprimer toutes les offres.
Corps
| Champ | Type | Requis | Notes |
|---|---|---|---|
offers | array (0 à 50) | ✅ | Offres dans l'ordre d'affichage. [] supprime toutes les offres. |
offers[].title | string | ✅ | Coupé à 255 caractères |
offers[].quantity | int, de 1 à 9999 | ✅ | Unités dans le lot. Chaque offre a besoin d'une quantité différente. |
offers[].price | number > 0 | ✅ sans discount_type | Total du lot entier, en DZD. Ignoré quand discount_type est défini. |
offers[].discount_type | amount | percent | null | Défaut null (prix fixe du lot) | |
offers[].discount_value | number > 0 | ✅ avec discount_type | En DZD pour amount, 100 au maximum pour percent |
offers[].compare_price | number ≥ 0 | null | Prix barré, en DZD | |
offers[].badge_text | string | Coupé à 100 caractères | |
offers[].badge_color | #rgb ou #rrggbb | Défaut #10b981 | |
offers[].free_shipping | bool | Livraison gratuite quand l'acheteur commande cette offre. Défaut false. | |
offers[].image_path | string | Garde l'image de l'offre : renvoyez le image_path donné par le GET | |
offers[].is_active | bool | Défaut true |
Les images d'offre ne peuvent pas être envoyées par l'API : ajoutez-les depuis le tableau de bord. Une image dont vous omettez le image_path est supprimée.
curl -X POST 'https://api.dzbuild.app/v1/products/26/offers' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"offers": [
{ "title": "Buy 2, get 1 free", "quantity": 3, "price": 2000,
"compare_price": 3000, "badge_text": "Best value", "free_shipping": true },
{ "title": "Pack of 2", "quantity": 2, "discount_type": "percent", "discount_value": 10 }
]
}'
Renvoie 200 avec les nouvelles offres, dans la forme du GET ci-dessus. Une charge utile rejetée laisse vos offres existantes intactes.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | offers absent ou pas une liste, plus de 50 offres, un title manquant, ou un quantity, price, discount_type, discount_value, compare_price ou badge_color invalide |
duplicate_offer_quantity | 422 | Deux offres avec la même quantity |
invalid_image_path | 422 | image_path n'est pas une image déjà utilisée par les offres de ce produit |
not_found | 404 | Produit absent de votre boutique |
GET /v1/products/{id}/addons
Lit les champs remplis par l'acheteur de ce produit : des champs supplémentaires que l'acheteur remplit sur la page produit (un texte court, un texte long ou l'envoi d'une image), et l'interrupteur enabled qui les affiche ou les masque.
Auth : clé plateforme avec products:read.
curl https://api.dzbuild.app/v1/products/26/addons \
-H "Authorization: Bearer $DZ_KEY"
{
"data": {
"product_id": 26,
"enabled": true,
"addons": [
{ "id": 7, "title": "Name to print", "input_type": "text",
"placeholder": "Up to 20 letters", "is_required": true, "extra_price": 300,
"max_length": 20, "allowed_extensions": null, "sort_order": 0, "is_active": true },
{ "id": 8, "title": "Your photo", "input_type": "image",
"placeholder": null, "is_required": false, "extra_price": 0,
"max_length": null, "allowed_extensions": "jpg,png", "sort_order": 1, "is_active": true }
]
}
}
La page produit n'affiche les champs que tant que enabled vaut true, et seulement ceux qui ont "is_active": true. extra_price s'ajoute à la commande quand l'acheteur remplit ce champ.
POST /v1/products/{id}/addons
Remplace les champs remplis par l'acheteur et peut régler l'interrupteur enabled dans le même appel. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.
Envoyez chaque champ à conserver, dans l'ordre d'affichage. Tout ce qui est omis est supprimé. Envoyez {"addons": []} pour supprimer tous les champs.
Corps
| Champ | Type | Requis | Notes |
|---|---|---|---|
enabled | bool | null | true affiche les champs sur la page produit, false les masque. Omis ou null, l'interrupteur reste tel quel. | |
addons | array (0 à 20) | ✅ | Champs dans l'ordre d'affichage. [] supprime tous les champs. |
addons[].title | string | ✅ | Coupé à 255 caractères |
addons[].input_type | text | textarea | image | Défaut text. textarea est un texte long, image l'envoi d'une image. | |
addons[].placeholder | string | Coupé à 255 caractères | |
addons[].is_required | bool | Défaut false | |
addons[].extra_price | number ≥ 0 | Montant en DZD ajouté quand l'acheteur remplit le champ. Défaut 0. | |
addons[].max_length | int ≥ 1 | Champs text et textarea uniquement. Plafonné à 65535. | |
addons[].allowed_extensions | liste ou chaîne séparée par des virgules | Champs image uniquement : parmi jpg, jpeg, png, gif, webp. Défaut les cinq. | |
addons[].is_active | bool | Défaut true |
curl -X POST 'https://api.dzbuild.app/v1/products/26/addons' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"enabled": true,
"addons": [
{ "title": "Name to print", "placeholder": "Up to 20 letters",
"is_required": true, "extra_price": 300, "max_length": 20 },
{ "title": "Your photo", "input_type": "image", "allowed_extensions": ["jpg", "png"] }
]
}'
Renvoie 200 avec les nouveaux champs et l'interrupteur, dans la forme du GET ci-dessus. Une charge utile rejetée laisse vos champs existants intacts.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | addons absent ou pas une liste, plus de 20 champs, un title manquant, un input_type inconnu, un extra_price ou un max_length invalide, un max_length sur un champ image, allowed_extensions sur un autre type de champ ou avec une autre extension |
not_found | 404 | Produit absent de votre boutique |
GET /v1/products/{id}/quantity-rules
Lit la quantité minimum et maximum de ce produit dans une seule commande. 0 signifie sans limite.
Auth : clé plateforme avec products:read.
curl https://api.dzbuild.app/v1/products/26/quantity-rules \
-H "Authorization: Bearer $DZ_KEY"
{
"data": {
"product_id": 26,
"min_qty": 2,
"max_qty": 10,
"has_rule": true,
"addon_id": "min-max-quantity",
"addon_active": false,
"warning": "The \"min-max-quantity\" addon is not active for this store, so this rule is stored but NOT enforced at checkout. Activate it from the dashboard Addons page."
}
}
La règle n'est appliquée à la commande que tant que l'add-on Quantité minimum & maximum par produit est actif sur la boutique. L'add-on est disponible sur tous les plans. addon_active indique s'il est actif, et warning apparaît quand une règle est enregistrée alors que l'add-on est désactivé.
POST /v1/products/{id}/quantity-rules
Fixe la quantité de commande minimum et maximum du produit. Auth : clé plateforme avec products:write. Nécessite Idempotency-Key.
Corps
| Champ | Type | Requis | Notes |
|---|---|---|---|
min_qty | int, de 0 à 10000 | 0 = pas de minimum. Omis, il compte comme 0. | |
max_qty | int, de 0 à 10000 | 0 = pas de maximum. Omis, il compte comme 0. Au-dessus de 0, il doit être au moins égal à min_qty. |
Chaque appel écrit les deux valeurs, alors envoyez-les ensemble : un champ omis devient 0. Envoyer 0 pour les deux supprime la règle.
curl -X POST 'https://api.dzbuild.app/v1/products/26/quantity-rules' \
-H "Authorization: Bearer $DZ_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "min_qty": 2, "max_qty": 10 }'
Renvoie 200 avec la règle enregistrée, dans la forme du GET ci-dessus.
Erreurs
| Code | HTTP | Cause |
|---|---|---|
validation_error | 422 | Une valeur qui n'est pas un entier, inférieure à 0 ou supérieure à 10000, ou un max_qty inférieur à min_qty (cette règle bloquerait toutes les commandes du produit) |
not_found | 404 | Produit absent de votre boutique |