Skip to main content

Landing pages

CRUD for landing pages — single-product conversion pages with sections (image carousels, order forms, fake visitors, countdowns, special offers).

Written by Support

A landing page is a focused, single-product conversion page. They're independent of the storefront catalogue — you can have a landing page with no live product (for upcoming launches), or one tied to a specific product for paid ads.

Sections (image carousels, order forms, fake visitors, countdowns, etc.) can be built through the API with the section endpoints below, or in the dashboard.

Plan limits

Plan

Landing pages (all statuses — drafts count)

Free

0 (one-time purchase: 1000 DZD/lifetime each)

Pro

3

Unlimited / Enterprise

unlimited

The cap applies on every create path, the API included, and counts every landing page including drafts. POST /v1/landing-pages (and POST /v1/landing-pages/generate) on a store at its limit answers 403 limit_reached. A Free-plan store can create a page only while it has an unused purchased slot; POST /v1/landing-pages marks that page is_purchased: true, which is what makes it visible on the storefront.

GET /v1/landing-pages

List landing pages. Cursor-paginated. Served fresh on every call, like GET /v1/landing-pages/{id}.

Auth: platform key with landing_pages:read, which GET /v1/landing-pages/{id} needs too. A key without it gets 403 forbidden.

Query parameters

Param

Type

Notes

limit

int 1–200

Default 50

cursor

string

Opaque

status

active | draft

Filter

An unrecognised status is ignored, returning all pages rather than a 400.

Response 200

{
  "data": {
    "items": [
      {
        "id":            42,
        "title":         "Black T-Shirt — 30% off",
        "slug":          "black-tshirt-30-off",
        "public_url":    "https://your-store.example.com/landing/black-tshirt-30-off",
        "status":        "active",
        "language":      "ar",
        "product_id":    26,
        "views":         1543,
        "is_purchased":  false,
        "created_at":    "2026-03-01 10:00:00",
        "updated_at":    "2026-03-15 14:22:11"
      }
    ],
    "next_cursor": null,
    "has_more": false
  }
}

GET /v1/landing-pages/{id}

Detail with section_count.

{
  "data": {
    "id":               42,
    "title":            "Black T-Shirt — 30% off",
    "slug":             "black-tshirt-30-off",
    "public_url":       "https://your-store.example.com/landing/black-tshirt-30-off",
    "status":           "active",
    "language":         "ar",
    "product_id":       26,
    "views":            1543,
    "is_purchased":     false,
    "meta_title":       "Black T-Shirt — Cotton 200gsm — 30% off | DZBuild",
    "meta_description": "Limited-time offer on our cotton black t-shirt.",
    "section_count":    7,
    "created_at":       "2026-03-01 10:00:00",
    "updated_at":       "2026-03-15 14:22:11"
  }
}

Field reference

Field

Notes

status

Strict active or draft only — there is no archived state for landing pages.

language

ar, fr or en.

public_url

Live address of the page: the store's address (its custom domain once it is live, otherwise its subdomain), then /landing/ and the slug. null while the store has no address. Use it as is rather than building the URL yourself.

product_id

The linked product, or null. An order-taking section (order_form, order_button, product_offers) needs this or its own settings.product_id. Once set, PATCH can switch it to another product but not back to null.

views

Read-only. Counted each time the public page is viewed; the API cannot write it and there is no way to reset it.

is_purchased

true once the page has been bought outright (1000 DZD/lifetime). On the Free plan this is what makes the page visible on the storefront.

section_count

Detail endpoint only — a live count of the page's sections, computed per request.

meta_title / meta_description

SEO tags. See the note under create.

POST /v1/landing-pages — create

Auth: platform key with landing_pages:write and landing_pages:read. The answer reads the page back, so with landing_pages:write alone the page is saved and the call answers 403 forbidden; the same holds for PATCH and /publish. Requires Idempotency-Key.

Body

Field

Type

Required

Notes

title

string, 1 to 255 bytes

✅

The limit counts bytes, not letters: an Arabic letter takes 2 bytes, so an Arabic title tops out near 127 letters

slug

string

Auto-derived from title if omitted. A slug you supply here is stored without normalisation — send a clean one

status

active | draft

Default draft. Any other value is silently coerced to draft

language

ar | fr | en

Default ar. Any other value is silently coerced to ar

product_id

int

Must belong to your store; the page links to this product

meta_title

string ≤ 255

SEO title. Omit it via the API and it is stored and returned as null (unlike the dashboard form, which copies title into it). The public page still renders title as a fallback, so the visible page title is correct either way

meta_description

string

SEO description

Slugs are made unique within your store by appending -2, -3, … An empty slug base falls back to landing- plus 6 hex characters.

Errors

Code

Cause

bad_request "Body must be valid JSON"

Wrong Content-Type or malformed JSON

bad_request "title is required (1-255 chars)"

Missing or over-long title

bad_request "product_id N does not belong to this store"

Cross-store id

limit_reached (403)

The store is at its plan's landing page limit (drafts count). On Free: no unused purchased slot is left

Request

curl -X POST 'https://api.dzbuild.app/v1/landing-pages' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "title":      "Black T-Shirt — 30% off",
    "language":   "ar",
    "product_id": 26,
    "status":     "draft"
  }'

Returns 200 (not 201) and the same shape as GET /v1/landing-pages/{id}. The new landing page has no sections: add them with the section endpoints below. The publish check does not run on create, so create the page as a draft and publish it once its sections are in place; a page created with status: active goes live empty.

PATCH /v1/landing-pages/{id}

Partial update.

PATCH validates more strictly than create: an invalid status returns 400 bad_request ("status must be active or draft") and an invalid language returns 400 ("language must be ar, fr, or en") instead of being coerced. title must still be 1 to 255 bytes. A slug sent on PATCH is normalised, unlike on create. Setting status to active runs the publish check (see /publish below). Once a page has a product, product_id: null answers 422 product_required; send another product id to switch products.

curl -X PATCH 'https://api.dzbuild.app/v1/landing-pages/42' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "title": "Black T-Shirt — Spring promo" }'

Renaming auto-regenerates slug only if you didn't pass slug explicitly. When the slug changes, by a rename or explicitly, the old address keeps working and redirects to the new one.

Sections

A page renders its sections from top to bottom. Section reads need landing_pages:read, section writes need landing_pages:write, and every write needs an Idempotency-Key.

Endpoint

Body

Answer

GET /v1/landing-page-section-types

200: section_types, each a type with its default_settings

GET /v1/landing-pages/{id}/sections

200: landing_page_id and sections in render order (up to 500, not paginated)

POST /v1/landing-pages/{id}/sections

section_type (or type), optional settings

201: the new section, added at the end of the page

POST /v1/landing-pages/{id}/sections/batch

sections: up to 30 objects with type (or section_type) and optional settings

201: the new sections. All or nothing: one bad entry saves none

POST /v1/landing-pages/{id}/sections/reorder

sections: every section id of the page, once each, in the new order

200: the sections in their new order

PATCH /v1/landing-pages/{id}/sections/{section_id}

settings, optional replace

200: the updated section

DELETE /v1/landing-pages/{id}/sections/{section_id}

200: { "deleted": true, "id": 901, "landing_page_id": 42 }

A section carries id, section_type, sort_order and settings. Answers that read the page back (the list, reorder and PATCH) also carry created_at and updated_at. The 14 types are image, order_form, order_button, free_text, contact_button, countdown, fake_visitors, special_offer, price_display, product_offers, custom_form, image_carousel, announcement_bar and testimonials. Read GET /v1/landing-page-section-types before writing, so your settings keys match what the page renders.

  • Settings you send are merged over the type's defaults, so one call can create a fully set up section. On PATCH they are merged over what is stored; send "replace": true to start again from the type's defaults. Nested objects merge key by key; lists such as slides, items, offers and fields are replaced whole. The section type cannot change.

  • An order_form, order_button or product_offers section needs a product: the page's product_id or its own settings.product_id. Without one the call answers 422 landing_page_has_no_product. A settings.product_id from another store answers 422 validation_error.

  • The order form fields show_name, show_phone and show_wilaya are always saved as true in any section that has them.

  • A slides list takes at most 20 entries and an items list at most 30 (422 too_many_items). Encoded settings may not pass 262144 bytes (422 settings_too_large). An unknown type answers 422 invalid_section_type.

  • On a write, a page outside your store answers 404 landing_page_not_found (the list and the check answer 404 not_found), and a section that is not on the page answers 404 section_not_found. A reorder list that repeats or leaves out a section answers 422 validation_error.

  • Section changes appear in GET /v1/changes. An edit, a deletion or a reorder can be undone with POST /v1/changes/{id}/undo, and a deleted section that comes back through an undo gets a new id. Adding a section cannot be undone: delete it instead.

curl -X POST 'https://api.dzbuild.app/v1/landing-pages/42/sections/batch' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "sections": [
      { "type": "announcement_bar" },
      { "type": "order_form" }
    ]
  }'

GET /v1/landing-pages/{id}/check

Reports what a buyer would find broken on the page. Auth: landing_pages:read.

The answer carries landing_page_id, status, product_id, sections (how many were checked), publishable, blocked_by (the first blocking message, or null) and problems. Each problem has code, severity (blocking or warning) and message; problems tied to one section also carry section_id.

Code

Severity

Meaning

empty_page

blocking

The page has no sections, so it renders blank

order_form_without_product

blocking

A section takes orders, but neither the page nor the section names a product, so orders would land at 0 DA

section_product_not_found

blocking

A section points at a product that is not in your store

no_order_form

warning

Nothing on the page can take an order

multiple_order_forms

warning

More than one order_form or order_button renders

variants_need_page_product

warning

A section names a product with variants while the page has no product. Variant pickers render only from the page's own product, so set product_id on the page

Publishing is refused while a blocking problem remains (see below).

POST /v1/landing-pages/{id}/publish

Convenience: flip status to active. Equivalent to PATCH ... { status: "active" }, and refused the same way: while GET /v1/landing-pages/{id}/check reports a blocking problem, the call answers 422 page_not_publishable and the error carries the problems list. Edits that do not send status skip this check, even on a live page.

curl -X POST 'https://api.dzbuild.app/v1/landing-pages/42/publish' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Idempotency-Key: publish-42-$(date +%s)"

POST /v1/landing-pages/generate

Builds a landing page from one of your products with AI. It answers 202 at once with a task to poll; a run takes about two minutes. Auth: the ai:generate scope. Keys created in the dashboard or with POST /v1/keys and third-party apps do not carry it and get 403 forbidden; Claude, ChatGPT and DZBuild Copilot connections carry it. Requires Idempotency-Key.

Field

Type

Required

Notes

title

string

✅

At least 3 characters; cut at 255

product_id

int

✅

An active product of your store with at least one image

description

string

Brief for the page, cut at 2000 characters

language

ar | fr | en

Default ar

size

medium | tall

Default medium. medium costs 20 AI credits, tall costs 45

The answer carries task_id, size, credits_charged, eta_seconds and poll. Credits are taken when the run starts and given back when the run fails or times out. Errors: 400 bad_request (title missing or shorter than 3 characters), 402 quota_exceeded (not enough AI credits), 403 limit_reached (plan landing page limit, with limit and current), 409 already_processing (one generation per store at a time), 422 product_required, product_not_found or product_has_no_image, 429 rate_limited or too_many_concurrent, 503 provider_unavailable (no credits taken).

Poll GET /v1/landing-pages/generate/{task_id} with landing_pages:read. It answers task_id, status (processing while the page is built, then completed or failed), landing_page_id once the page exists, current_step and error. A run still processing after 10 minutes is marked failed with error timeout at the next poll, and its credits are refunded.

DELETE /v1/landing-pages/{id}

Hard delete. The page's sections are removed with it.

curl -X DELETE 'https://api.dzbuild.app/v1/landing-pages/42' \
  -H "Authorization: Bearer $DZ_KEY" \
  -H "Idempotency-Key: del-42"

Response: { "data": { "deleted": true, "id": 42 } }.

Did this answer your question?