Products
The product is the core sellable unit on a store. All product calls are scoped to the calling key's store — you can never accidentally touch another merchant's data.
GET /v1/products
List products. Cursor-paginated. Cached at the edge for 30 s.
Auth: platform key with products:read.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
limit | int (1–200) | 50 | Page size |
cursor | string | — | From a prior response's next_cursor |
status | active | draft | archived | — | Filter by status |
search | string | — | Match against name (LIKE) and exact sku |
Request
curl 'https://api.dzbuild.app/v1/products?limit=10&status=active' \
-H "Authorization: Bearer $DZ_KEY"
Response 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": "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" }
}
primary_image is a filename. Resolve to a URL with https://cdn.dzbuild.app/<store_id>/products/<filename> (or your custom CDN domain). The full path is in images[].url on GET /v1/products/{id}.
GET /v1/products/{id}
Full product detail including images and variants.
Auth: platform key with products:read.
Request
curl https://api.dzbuild.app/v1/products/26 \
-H "Authorization: Bearer $DZ_KEY"
Response 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": "13_1768313552_b33d660c_1562f6687591.webp",
"is_primary": true, "sort_order": 0 }
],
"variants": [
{
"id": 11,
"name": "Duration",
"type": "text",
"options": [
{ "id": 14, "value": "30 days", "color_code": null, "image_id": null, "stock": null },
{ "id": 15, "value": "90 days", "color_code": null, "image_id": null, "stock": null },
{ "id": 16, "value": "180 days", "color_code": null, "image_id": null, "stock": null },
{ "id": 17, "value": "365 days", "color_code": null, "image_id": null, "stock": null }
]
}
],
"created_at": "2026-01-13 15:06:06",
"updated_at": "2026-01-13 15:12:32"
}
}
POST /v1/products — create
Auth: platform key with products:write. Requires Idempotency-Key.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (1–255) | ✅ | |
price | number ≥ 0 | ✅ | DZD |
compare_price | number ≥ 0 | null | Strike-through price | |
cost_price | number ≥ 0 | null | Internal only — never shown to customers | |
description | string | Long-form, can include line breaks | |
short_description | string ≤ 500 | One-liner | |
sku | string ≤ 100 | Internal SKU | |
barcode | string ≤ 100 | UPC/EAN | |
weight | number | kg, for shipping | |
shipping_height / width / length | number | cm | |
do_insurance | bool | Force shipping insurance on this item | |
track_stock | bool | Default false | |
stock_quantity | int ≥ 0 | If track_stock | |
low_stock_alert | int ≥ 0 | Default 5. Triggers product.stock_low webhook. | |
variant_stock_enabled | bool | Per-variant SKUs | |
combination_stock_enabled | bool | Per (color × size) combo SKUs. Implies variant_stock_enabled. | |
category_id | int | Must exist in your store | |
status | active | draft | archived | Default draft | |
featured | bool | Default false |
When variant_stock_enabled or combination_stock_enabled is true, track_stock is auto-disabled (variants own their own stock).
Plan limit
Free: 5 active products. Pro: 300. Unlimited / Enterprise: unlimited. The check counts only status = 'active' rows. Drafts don't count. Hitting the limit returns:
{ "error": { "code": "bad_request",
"message": "Plan 'free' allows at most 5 active products. Upgrade to add more." } }
Request
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"
}'
Response 201
Echoes the same shape as GET /v1/products/{id}. id, slug, and created_at are now populated. slug is auto-generated from name and made unique within your store. You can override by passing your own slug in the body (subject to [a-z0-9-] normalisation).
Errors
| Code | Cause |
|---|---|
bad_request "Body must be valid JSON" | Wrong Content-Type or malformed JSON |
bad_request "name is required (1-255 chars)" | Missing or over-long name |
bad_request "price must be a non-negative number" | Bad price |
bad_request "category_id N does not belong to this store" | Cross-store id |
bad_request "Plan 'free' allows at most …" | Plan limit |
PATCH /v1/products/{id} — update
Auth: platform key with products:write. Requires Idempotency-Key.
Partial update — send only the fields you want to change. Unspecified fields are preserved.
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" }'
Returns 200 and the full updated product. If the product doesn't exist (or belongs to another store) you get 404 not_found.
Renaming via PATCH { name: ... } automatically regenerates the slug only if you didn't pass slug explicitly. Pass slug if you want to preserve a specific URL after a rename.
DELETE /v1/products/{id}
Auth: platform key with products:write. Requires 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"
Response:
{ "data": { "deleted": true, "id": 26 } }
This is a hard delete — the product row is removed, and FK cascades clean up images, variants, offers, and combinations. Image files in R2 are left for an async cleanup sweep (we don't block the API call on R2 deletion).
POST /v1/products/{id}/images (coming v1.1)
Will return a presigned R2 PUT URL so your client uploads files directly to Cloudflare R2 — no Algerian-origin round-trip, no FPM-blocking image processing.
Until then, image uploads go through the dashboard at Products → Edit → Images.