Skip to main content

Products

Full CRUD for the product catalog — list, get, create, update, delete. With variants, images, plan limits.

Written by Support

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. Served fresh on every call: a GET sent right after a write returns the new values.

Auth: platform key with products:read (granted by default). A key without it gets 403 forbidden.

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

An unrecognised status is ignored rather than rejected — you get the unfiltered list, which includes archived products. Filter explicitly if you only want live items.

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":  "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" }
}

ℹ️ Info — Changed in v1.1 — image URLs are now complete

primary_image (and images[].url on GET /v1/products/{id}) is now a full CDN URL, ready to use as-is. Before v1.1 both returned a bare filename that callers had to prefix themselves. If your integration builds the prefix manually, drop that logic — the value already starts with https://.

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": "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"
  }
}

ℹ️ Info — Added in v1.1

images[].alt_text, the full option fields (price_adjustment, sku, show_as_card, sort_order, is_active), group required / sort_order, and the whole combinations block are new. combinations lists at most 300 entries — combination_count is always the true total and combinations_truncated tells you when the list was cut.

POST /v1/products — create

Auth: platform key with products:write and products:read. The reply is the product as GET /v1/products/{id} returns it, so a key without products:read gets 403 forbidden even though the product was created. 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 and HTML formatting (bold, lists, headings, links, tables, images); scripts and other unsafe tags are removed. Max 60,000 bytes: longer returns bad_request "description exceeds 60000 bytes".

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. Drives the dashboard's low-stock badge.

variant_stock_enabled

bool

Track stock per variant option (Red, L, …)

combination_stock_enabled

bool

Track stock per variant combination (Red+L). Implies variant_stock_enabled.

category_id

int

Must exist in your store. The product joins that category, which becomes its main one; categories it already belongs to are kept. On PATCH, null clears the main category without removing the product from any category.

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

You rarely need these two flags directly: PUT /v1/products/{id}/variants sets them for you based on the payload you send (per-option stock or combinations).

Plan limit

Free: 5 active products. Pro: 300. Unlimited / Enterprise: unlimited. The check counts only products with status: "active", drafts don't count, and the count is always live at the moment of the call. The check runs on create only: flipping an existing draft to active via PATCH is never blocked, so a free-plan store can exceed 5 active products that way. Because it runs on every create, a store at its limit cannot create a new product even with status: "draft". An unrecognised plan name falls back to the free limit of 5. 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 200

A successful create returns HTTP 200 (not 201) with the same body as GET /v1/products/{id}. Do not branch on status === 201 — check data.id instead. id, slug, and created_at are now populated.

On create, slug is always derived from name — a slug in the body is ignored. To set a specific slug, create first, then PATCH /v1/products/{id} with {"slug":"…"}. Normalisation lowercases and replaces every run of non-letter/non-digit characters with - (Unicode-aware — Arabic and accented letters are preserved, so it is NOT [a-z0-9-]), trimming to 200 characters; collisions get -2, -3, … suffixes.

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 and products:read. The reply is the updated product, so a key without products:read gets 403 forbidden even though the change was saved. 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 is removed along with its images, variants, offers, add-ons, combinations and customer reviews. The stored image files are not removed by this call, so an image URL you saved earlier can keep loading.

⚠️ Warning — Deletion detaches history; a product used by a landing page cannot be deleted

Past orders keep their line items, and the product name / SKU / price captured at purchase time stays intact, so old orders still read correctly, but the line no longer links to a product (product_id becomes null). A product that a landing page uses (page-level, or in an order form, order button or product offers section) is refused with 409 product_in_use_by_landing_page; the error data lists the pages in landing_pages[] with id, title and slug. Delete that landing page first (DELETE /v1/landing-pages/{id}) or attach another product to it (PATCH /v1/landing-pages/{id} with a new product_id), then delete the product. Prefer PATCH { "status": "archived" } over deletion.

POST /v1/products/{id}/images — add an image

Added in v1.1. Auth: platform key with products:write. Requires Idempotency-Key.

You give a public https URL; DZBuild downloads the image server-side, converts and optimises it, and hosts it on the store CDN. There is no file upload through the API — link to the image and we fetch it.

Body

Field

Type

Required

Notes

url

string ≤ 2000

✅

Public https:// link to the image file

alt_text

string ≤ 255

Accessibility / SEO text

is_primary

bool

Make this the main product photo

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

Rules worth knowing:

  • The first image of a product automatically becomes the primary one.

  • Posting a URL whose bytes are already attached to the product does not create a duplicate — you get the existing image back with "deduplicated": true (HTTP 200 instead of 201).

  • Accepted formats: JPEG, PNG, WebP, GIF, BMP, AVIF, HEIC/HEIF, TIFF. Max 20 MB and 10000×10000 px. Images are converted to WebP (EXIF stripped), and an image wider than 2000 px is scaled down to 2000 px wide, keeping its proportions. A WebP file of 3 MB or less and no wider than 2000 px is stored as sent.

  • Maximum 20 images per product.

Which URLs are accepted

For security, the fetcher only accepts public addresses and never follows redirects. A URL is refused (url_refused) when it is not https, carries credentials (https://user:pass@…), uses a port other than 443, is an IP address rather than a hostname, or resolves to a private / internal / cloud-metadata address. A link that answers with a redirect or an error status fails with image_fetch_failed; a link that answers with a web page (a login page, for example) or any other file that isn't a supported image fails with unsupported_image.

Errors

Code

HTTP

Cause

validation_error

422

url missing or longer than 2000 characters

url_refused

422

URL rejected by the rules above

image_fetch_failed

422

Host unreachable, redirect, or non-200

unsupported_image

422

Not an image (a web page, for example), unsupported format, or dimensions out of range

image_too_large

422

Over 20 MB

too_many_images

422

Product already has 20 images

not_found

404

Product not in your store

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

Added in v1.1. Update alt_text, sort_order (0–999), or promote the image with 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 }'

A product always keeps exactly one primary image, so is_primary: false is rejected with primary_required — promote a different image instead.

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

Added in v1.1. Deletes the image row and its stored files.

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

If variant options pointed at this image, those links are cleared (the options themselves survive) — variant_references_cleared tells you how many. Deleting the primary image automatically promotes the next one.

PUT /v1/products/{id}/variants — replace variants

Added in v1.1. Auth: platform key with products:write. Idempotency-Key is optional: with one, a retry with the same value gets the first answer back; without one, every call runs the full replace again.

⚠️ Warning — This replaces ALL variants of the product

There is no partial variant update. Read the current state with GET /v1/products/{id} and send back everything you want to keep — anything omitted is deleted. Send {"groups": []} to remove all variants.

Body

Field

Type

Required

Notes

groups

array

✅

Variant groups in display order. [] clears all variants.

groups[].name

string ≤ 100

✅

e.g. Color, Size. Unique per product.

groups[].type

text | color | image_text | selectable | dropdown

Default text. selectable = optional multi-select add-on group. dropdown = text options shown as a select list.

groups[].required

bool

Default true (always false for selectable)

groups[].options[].name

string ≤ 100

✅

Unique inside the group

groups[].options[].color_code

#rrggbb

For color groups

groups[].options[].price_adjustment

number

Added to (or subtracted from) the base price

groups[].options[].stock

int ≥ 0 | null

Per-option stock

groups[].options[].sku

string ≤ 100

Per-option SKU

groups[].options[].image_id

int

Must be an existing image of this product

groups[].options[].show_as_card

bool

Render the option as an image card

combinations

array

Per-combination stock (needs 2+ non-selectable groups)

combinations[].options

object

✅

{ "Color": "Red", "Size": "L" } — one entry per non-selectable group

combinations[].stock

int ≥ 0

✅

combinations[].sku

string ≤ 100

combinations[].is_active

bool

Default true

Limits: 10 groups, 100 options per group, 200 options total, 1000 combinations.

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 }
    ]
  }'

Returns the new variants + combinations block (same shape as GET /v1/products/{id}).

Stock mode is set for you

  • Combinations sent → per-combination stock (combination_stock_enabled), product-level track_stock off.

  • No combinations, but options carry stock → per-option stock (variant_stock_enabled), track_stock off.

  • Neither → variants are presentation-only; product-level stock keeps working.

Errors

Code

HTTP

Cause

validation_error

422

Bad names, types, colours, numbers, or a limit exceeded

invalid_image_id

422

image_id is not an image of this product

combinations_not_applicable

422

Combinations sent with fewer than 2 non-selectable groups

duplicate_combination

422

Two combinations with the same option set

not_found

404

Product not in your store

Validation runs before anything is deleted — a rejected payload leaves your existing variants untouched.

GET /v1/products/{id}/stock

Reads the product's stock mode and the current count of every target you can set in that mode. Read it before a stock sync to get the option and combination ids. Unlike GET /v1/products, this read is not cached, so it shows a change at once.

Auth: platform key with 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 says where the product's stock is counted, and the answer lists the targets of that mode:

mode

Stock is counted on

Listed in the answer

product

The product itself

product.stock_quantity

variant_options

Each variant option

options[]

combinations

Each combination of options

combinations[] with sku, is_active and options (group name to option name). options[] is listed too, for reading only.

An option with "stock": null and "unlimited": true has unlimited stock. The mode follows the variants saved with PUT /v1/products/{id}/variants (see above).

POST /v1/products/{id}/stock

Sets or shifts stock counts. Auth: platform key with products:write. Requires Idempotency-Key.

Body

items lists 1 to 500 targets. Each item carries exactly one of set, delta or "unlimited": true, and a target may appear only once per request.

Field

Type

Required

Notes

items[].target

product | option | combination

✅

Must match the product's mode: product, option for variant_options, combination for combinations

items[].id

int ≥ 1

✅ for option and combination

The id from GET /v1/products/{id}/stock

items[].set

int, 0 to 9999999

New count

items[].delta

int, not 0

Units to add, negative to remove. The result stays between 0 and 9999999.

items[].unlimited

bool

Options only. true makes the option unlimited. An option that is unlimited today needs "unlimited": false next to set to start counting.

A set or delta on the product target also turns track_stock on, so the store counts that product's stock from then on.

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 }
    ]
  }'

Returns 200 with the stock after the change, in the GET shape above: option 41 now reads 20 and option 42 reads 8. The items are saved together: if one is refused, none is saved. The change goes into the store's change history (GET /v1/changes), and POST /v1/changes/{id}/undo puts the previous counts back.

Errors

Code

HTTP

Cause

validation_error

422

items missing, empty or longer than 500; a bad target, id, set or delta; the same target twice; not exactly one of set, delta, "unlimited": true; "unlimited": true on a product or combination

option_stock_unlimited

422

The option is unlimited today: a set without "unlimited": false, or any delta

stock_mode_mismatch

409

target does not match the product's stock mode

not_found

404

Product not in your store, or the option or combination is not part of this product

GET /v1/products/{id}/offers

Reads the product's quantity offers, the bundles shown on the product page.

Auth: platform key with 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 }
    ]
  }
}

⚠️ Warning — price is the bundle total

price is what the buyer pays for all quantity units together, not a unit price. On a 1000 DZD product, "Buy 2, get 1 free" is "quantity": 3, "price": 2000. An offer with a discount_type stores price as 0 and takes discount_value off the product price times the quantity: amount subtracts DZD, percent takes a percentage and also scales variant price adjustments. The second offer above sells 2 units for 1800 DZD.

image_url is the full CDN link of the offer picture, null when it has none.

POST /v1/products/{id}/offers

Replaces the product's quantity offers. Auth: platform key with products:write. Requires Idempotency-Key.

⚠️ Warning — This replaces ALL offers of the product

Send every offer you want to keep, in display order. Anything omitted is deleted. Send {"offers": []} to remove all offers.

Body

Field

Type

Required

Notes

offers

array (0 to 50)

✅

Offers in display order. [] removes all offers.

offers[].title

string

✅

Cut to 255 characters

offers[].quantity

int, 1 to 9999

✅

Units in the bundle. Each offer needs a different quantity.

offers[].price

number > 0

✅ without discount_type

Total for the whole bundle, in DZD. Ignored when discount_type is set.

offers[].discount_type

amount | percent | null

Default null (fixed bundle price)

offers[].discount_value

number > 0

✅ with discount_type

DZD for amount, at most 100 for percent

offers[].compare_price

number ≥ 0 | null

Strike-through price, in DZD

offers[].badge_text

string

Cut to 100 characters

offers[].badge_color

#rgb or #rrggbb

Default #10b981

offers[].free_shipping

bool

Delivery is free when the buyer orders this offer. Default false.

offers[].image_path

string

Keeps the offer picture: send back the image_path that GET returned

offers[].is_active

bool

Default true

Offer pictures cannot be uploaded through the API: add them in the dashboard. A picture whose image_path you leave out is deleted.

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 }
    ]
  }'

Returns 200 with the new offers, in the GET shape above. A rejected payload leaves your existing offers untouched.

Errors

Code

HTTP

Cause

validation_error

422

offers missing or not a list, more than 50 offers, a missing title, or a bad quantity, price, discount_type, discount_value, compare_price or badge_color

duplicate_offer_quantity

422

Two offers with the same quantity

invalid_image_path

422

image_path is not a picture this product's offers already use

not_found

404

Product not in your store

GET /v1/products/{id}/addons

Reads the product's buyer input fields: extra fields the buyer fills in on the product page (a short text, a long text or a picture upload), and the enabled switch that shows or hides them.

Auth: platform key with 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 }
    ]
  }
}

The product page shows the fields only while enabled is true, and only the fields with "is_active": true. extra_price is added to the order when the buyer fills that field.

POST /v1/products/{id}/addons

Replaces the product's buyer input fields and can set the enabled switch in the same call. Auth: platform key with products:write. Requires Idempotency-Key.

⚠️ Warning — This replaces ALL input fields of the product

Send every field you want to keep, in display order. Anything omitted is deleted. Send {"addons": []} to remove all fields.

Body

Field

Type

Required

Notes

enabled

bool | null

true shows the fields on the product page, false hides them. Omitted or null leaves the switch as it is.

addons

array (0 to 20)

✅

Fields in display order. [] removes all fields.

addons[].title

string

✅

Cut to 255 characters

addons[].input_type

text | textarea | image

Default text. textarea is a long text, image a picture upload.

addons[].placeholder

string

Cut to 255 characters

addons[].is_required

bool

Default false

addons[].extra_price

number ≥ 0

DZD added when the buyer fills the field. Default 0.

addons[].max_length

int ≥ 1

text and textarea fields only. Capped at 65535.

addons[].allowed_extensions

list or comma-separated string

image fields only: any of jpg, jpeg, png, gif, webp. Default all five.

addons[].is_active

bool

Default 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"] }
    ]
  }'

Returns 200 with the new fields and the switch, in the GET shape above. A rejected payload leaves your existing fields untouched.

Errors

Code

HTTP

Cause

validation_error

422

addons missing or not a list, more than 20 fields, a missing title, an unknown input_type, a bad extra_price or max_length, a max_length on an image field, allowed_extensions on another field type or with another extension

not_found

404

Product not in your store

GET /v1/products/{id}/quantity-rules

Reads the minimum and maximum quantity of this product that one order can contain. 0 means no limit.

Auth: platform key with 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."
  }
}

Checkout applies the rule only while the Minimum & Maximum Quantity Per Product add-on is active on the store. The add-on is available on every plan. addon_active tells you whether it is on, and warning appears when a rule is saved but the add-on is off.

POST /v1/products/{id}/quantity-rules

Sets the product's minimum and maximum order quantity. Auth: platform key with products:write. Requires Idempotency-Key.

Body

Field

Type

Required

Notes

min_qty

int, 0 to 10000

0 = no minimum. Omitted counts as 0.

max_qty

int, 0 to 10000

0 = no maximum. Omitted counts as 0. Above 0, it must be at least min_qty.

Each call writes both values, so send them together: a field you leave out becomes 0. Sending 0 for both removes the rule.

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 }'

Returns 200 with the saved rule, in the GET shape above.

Errors

Code

HTTP

Cause

validation_error

422

A value that is not a whole number, below 0 or above 10000, or a max_qty below min_qty (that rule would block every order of the product)

not_found

404

Product not in your store

Did this answer your question?