Landing pages
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, fake visitors, countdowns, etc.) are managed in the dashboard at v1; the API CRUDs the parent record only. A v1.1 update will expose section CRUD too.
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 is enforced by the dashboard create/duplicate flows only, counting every landing page including drafts. The API enforces nothing: POST /v1/landing-pages followed by /publish bypasses the cap entirely, and on a paid plan the extra pages render live on the storefront. Free-plan pages stay invisible unless the page was purchased (is_purchased).
GET /v1/landing-pages
List landing pages. Cursor-paginated. Cached for 30 s — check the X-Cache: HIT|MISS response header. GET /v1/landing-pages/{id} is not cached.
Auth: any active platform key for the store (landing_pages:read is not enforced at v1; only landing_pages:write is checked, on the write endpoints).
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",
"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",
"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. |
product_id | The linked product, or null. A page with no product id cannot price its order form correctly. |
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. Requires Idempotency-Key.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
title | string 1–255 | ✅ | |
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 |
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 zero sections — populate them in the dashboard.
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–255 characters. A slug sent on PATCH is normalised, unlike on create.
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.
POST /v1/landing-pages/{id}/publish
Convenience: flip status to active. Equivalent to PATCH ... { status: "active" }.
curl -X POST 'https://api.dzbuild.app/v1/landing-pages/42/publish' \
-H "Authorization: Bearer $DZ_KEY" \
-H "Idempotency-Key: publish-42-$(date +%s)"
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 } }.