# Landing pages

Une landing page est une page de conversion focalisée sur un seul produit. Elles sont indépendantes du catalogue boutique — vous pouvez avoir une landing page sans produit en ligne (pour des lancements à venir), ou une liée à un produit pour des pubs payantes.

Les sections (carrousels, formulaires de commande, faux visiteurs, comptes à rebours, etc.) se construisent par l'API avec les endpoints de sections ci-dessous, ou dans le tableau de bord.

## Limites par plan[​](#limites-par-plan "Lien direct vers Limites par plan")

| Plan                   | Landing pages (tous statuts — les brouillons comptent) |
| ---------------------- | ------------------------------------------------------ |
| Free                   | 0 (achat unique : 1000 DZD / à vie chacune)            |
| Pro                    | 3                                                      |
| Unlimited / Enterprise | illimité                                               |

Le plafond s'applique sur toutes les voies de création, API comprise, et compte chaque landing page, brouillons inclus. Sur une boutique à sa limite, `POST /v1/landing-pages` (et `POST /v1/landing-pages/generate`) répond `403 limit_reached`. Une boutique sur le plan Free ne peut créer une page que tant qu'il lui reste un achat de landing page non utilisé ; `POST /v1/landing-pages` marque cette page `is_purchased: true`, et c'est ce qui la rend visible sur la vitrine.

## `GET /v1/landing-pages`[​](#get-v1landing-pages "Lien direct vers get-v1landing-pages")

Liste les landing pages. Pagination par curseur. Servi à neuf à chaque appel, comme `GET /v1/landing-pages/{id}`.

**Auth :** clé plateforme avec `landing_pages:read`, que `GET /v1/landing-pages/{id}` demande aussi. Une clé sans ce scope reçoit `403 forbidden`.

### Paramètres de requête[​](#paramètres-de-requête "Lien direct vers Paramètres de requête")

| Param    | Type                | Notes     |
| -------- | ------------------- | --------- |
| `limit`  | int 1–200           | Défaut 50 |
| `cursor` | string              | Opaque    |
| `status` | `active` \| `draft` | Filtre    |

Un `status` non reconnu est ignoré : toutes les pages sont renvoyées plutôt qu'un `400`.

### Réponse 200[​](#réponse-200 "Lien direct vers Réponse 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}`[​](#get-v1landing-pagesid "Lien direct vers get-v1landing-pagesid")

Détail avec `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"

  }

}
```

### Référence des champs[​](#référence-des-champs "Lien direct vers Référence des champs")

| Champ                             | Notes                                                                                                                                                                                                                                                                    |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`                          | Strictement `active` ou `draft` — il n'y a pas d'état `archived` pour les landing pages.                                                                                                                                                                                 |
| `language`                        | `ar`, `fr` ou `en`.                                                                                                                                                                                                                                                      |
| `public_url`                      | Adresse en ligne de la page : l'adresse de la boutique (son domaine personnalisé une fois actif, sinon son sous-domaine), puis `/landing/` et le slug. `null` tant que la boutique n'a pas d'adresse. Utilisez-la telle quelle plutôt que de construire l'URL vous-même. |
| `product_id`                      | Le produit lié, ou `null`. Une section qui prend des commandes (`order_form`, `order_button`, `product_offers`) a besoin de lui ou de son propre `settings.product_id`. Une fois défini, PATCH peut le changer pour un autre produit, mais pas le remettre à `null`.     |
| `views`                           | **Lecture seule.** Compté à chaque consultation de la page publique ; l'API ne peut pas l'écrire et il n'existe aucun moyen de le remettre à zéro.                                                                                                                       |
| `is_purchased`                    | `true` une fois la page achetée définitivement (1000 DZD / à vie). Sur le plan Free, c'est ce qui rend la page visible sur la vitrine.                                                                                                                                   |
| `section_count`                   | Endpoint détail uniquement — un comptage en direct des sections de la page, calculé à chaque requête.                                                                                                                                                                    |
| `meta_title` / `meta_description` | Balises SEO. Voir la note sous création.                                                                                                                                                                                                                                 |

## `POST /v1/landing-pages` — créer[​](#post-v1landing-pages--créer "Lien direct vers post-v1landing-pages--créer")

**Auth :** clé plateforme avec `landing_pages:write` et `landing_pages:read`. La réponse relit la page : avec `landing_pages:write` seul, la page est enregistrée et l'appel répond `403 forbidden` ; il en va de même pour PATCH et `/publish`. **Nécessite `Idempotency-Key`.**

### Corps[​](#corps "Lien direct vers Corps")

| Champ              | Type                   | Requis | Notes                                                                                                                                                                                                                                               |
| ------------------ | ---------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`            | string, 1 à 255 octets | ✅     | La limite compte les octets, pas les lettres : une lettre arabe prend 2 octets, un titre arabe plafonne donc vers 127 lettres                                                                                                                       |
| `slug`             | string                 |        | Auto-déduit de `title` si omis. Un slug que vous fournissez ici est stocké **sans** normalisation — envoyez-en un propre                                                                                                                            |
| `status`           | `active` \| `draft`    |        | Défaut `draft`. Toute autre valeur est silencieusement ramenée à `draft`                                                                                                                                                                            |
| `language`         | `ar` \| `fr` \| `en`   |        | Défaut `ar`. Toute autre valeur est silencieusement ramenée à `ar`                                                                                                                                                                                  |
| `product_id`       | int                    |        | Doit appartenir à votre boutique ; la page lie ce produit                                                                                                                                                                                           |
| `meta_title`       | string ≤ 255           |        | Titre SEO. Omis via l'API, il est stocké et renvoyé comme `null` (contrairement au formulaire du tableau de bord, qui y recopie `title`). La page publique affiche quand même `title` en repli, le titre visible est donc correct dans les deux cas |
| `meta_description` | string                 |        | Description SEO                                                                                                                                                                                                                                     |

Les slugs sont rendus uniques dans votre boutique par ajout de `-2`, `-3`, … Une base de slug vide retombe sur `landing-` suivi de 6 caractères hex.

### Erreurs[​](#erreurs "Lien direct vers Erreurs")

| Code                                                        | Cause                                                                                                                                      |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `bad_request` "Body must be valid JSON"                     | Content-Type incorrect ou JSON malformé                                                                                                    |
| `bad_request` "title is required (1-255 chars)"             | Titre manquant ou trop long                                                                                                                |
| `bad_request` "product\_id N does not belong to this store" | ID cross-boutique                                                                                                                          |
| `limit_reached` (403)                                       | La boutique a atteint la limite de landing pages de son plan (brouillons compris). Sur Free : plus aucun achat de landing page non utilisé |

### Requête[​](#requête "Lien direct vers Requête")

```
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"

  }'
```

Renvoie **200** (et non 201) avec la même forme que `GET /v1/landing-pages/{id}`. La nouvelle landing page n'a aucune section : ajoutez-les avec les endpoints de sections ci-dessous. Le contrôle de publication ne tourne pas à la création : créez la page en `draft` et publiez-la une fois ses sections en place ; une page créée avec `status: active` est en ligne vide.

## `PATCH /v1/landing-pages/{id}`[​](#patch-v1landing-pagesid "Lien direct vers patch-v1landing-pagesid")

Mise à jour partielle.

PATCH valide plus strictement que la création : un `status` invalide renvoie `400 bad_request` (« status must be active or draft ») et un `language` invalide renvoie `400` (« language must be ar, fr, or en ») au lieu d'être coercé. `title` doit toujours faire 1 à 255 octets. Un `slug` envoyé sur PATCH **est** normalisé, contrairement à la création. Passer `status` à `active` lance le contrôle de publication (voir `/publish` plus bas). Une fois qu'une page a un produit, `product_id: null` répond `422 product_required` ; envoyez un autre id de produit pour changer de produit.

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

Renommer régénère `slug` automatiquement uniquement si vous n'avez pas envoyé `slug` explicitement. Quand le slug change, par un renommage ou explicitement, l'ancienne adresse continue de fonctionner et redirige vers la nouvelle.

## Sections[​](#sections "Lien direct vers Sections")

Une page affiche ses sections de haut en bas. Les lectures de sections demandent `landing_pages:read`, les écritures `landing_pages:write`, et chaque écriture demande une `Idempotency-Key`.

| Endpoint                                              | Corps                                                                                   | Réponse                                                                                             |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `GET /v1/landing-page-section-types`                  |                                                                                         | `200` : `section_types`, chacun un `type` avec ses `default_settings`                               |
| `GET /v1/landing-pages/{id}/sections`                 |                                                                                         | `200` : `landing_page_id` et `sections` dans l'ordre d'affichage (jusqu'à 500, sans pagination)     |
| `POST /v1/landing-pages/{id}/sections`                | `section_type` (ou `type`), `settings` facultatif                                       | `201` : la nouvelle `section`, ajoutée en bas de la page                                            |
| `POST /v1/landing-pages/{id}/sections/batch`          | `sections` : jusqu'à 30 objets avec `type` (ou `section_type`) et `settings` facultatif | `201` : les nouvelles `sections`. Tout ou rien : une seule entrée invalide et rien n'est enregistré |
| `POST /v1/landing-pages/{id}/sections/reorder`        | `sections` : tous les ids de sections de la page, une fois chacun, dans le nouvel ordre | `200` : les sections dans leur nouvel ordre                                                         |
| `PATCH /v1/landing-pages/{id}/sections/{section_id}`  | `settings`, `replace` facultatif                                                        | `200` : la `section` mise à jour                                                                    |
| `DELETE /v1/landing-pages/{id}/sections/{section_id}` |                                                                                         | `200` : `{ "deleted": true, "id": 901, "landing_page_id": 42 }`                                     |

Une section porte `id`, `section_type`, `sort_order` et `settings`. Les réponses qui relisent la page (la liste, le reorder et PATCH) portent aussi `created_at` et `updated_at`. Les 14 types sont `image`, `order_form`, `order_button`, `free_text`, `contact_button`, `countdown`, `fake_visitors`, `special_offer`, `price_display`, `product_offers`, `custom_form`, `image_carousel`, `announcement_bar` et `testimonials`. Lisez `GET /v1/landing-page-section-types` avant d'écrire, pour que vos clés de réglages correspondent à ce que la page affiche.

* Les réglages envoyés sont fusionnés avec les valeurs par défaut du type : un seul appel suffit pour créer une section entièrement réglée. Sur PATCH, ils sont fusionnés avec ceux enregistrés ; envoyez `"replace": true` pour repartir des valeurs par défaut du type. Les objets imbriqués fusionnent clé par clé ; les listes comme `slides`, `items`, `offers` et `fields` sont remplacées en entier. Le type d'une section ne peut pas changer.
* Une section `order_form`, `order_button` ou `product_offers` a besoin d'un produit : le `product_id` de la page ou son propre `settings.product_id`. Sans produit, l'appel répond `422 landing_page_has_no_product`. Un `settings.product_id` d'une autre boutique répond `422 validation_error`.
* Les champs du formulaire de commande `show_name`, `show_phone` et `show_wilaya` sont toujours enregistrés à `true` dans toute section qui les porte.
* Une liste `slides` accepte au plus 20 entrées et une liste `items` au plus 30 (`422 too_many_items`). Les réglages encodés ne peuvent pas dépasser 262144 octets (`422 settings_too_large`). Un type inconnu répond `422 invalid_section_type`.
* Sur une écriture, une page hors de votre boutique répond `404 landing_page_not_found` (la liste et le contrôle répondent `404 not_found`), et une section absente de la page répond `404 section_not_found`. Une liste de reorder qui répète ou oublie une section répond `422 validation_error`.
* Les changements de sections apparaissent dans `GET /v1/changes`. Une modification, une suppression ou un reorder s'annule avec `POST /v1/changes/{id}/undo`, et une section supprimée qui revient par une annulation reçoit un nouvel id. L'ajout d'une section ne s'annule pas : supprimez-la plutôt.

```
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`[​](#get-v1landing-pagesidcheck "Lien direct vers get-v1landing-pagesidcheck")

Signale ce qu'un acheteur trouverait cassé sur la page. **Auth :** `landing_pages:read`.

La réponse porte `landing_page_id`, `status`, `product_id`, `sections` (le nombre de sections contrôlées), `publishable`, `blocked_by` (le premier message bloquant, ou `null`) et `problems`. Chaque problème a `code`, `severity` (`blocking` ou `warning`) et `message` ; les problèmes liés à une section portent aussi `section_id`.

| Code                         | Gravité    | Sens                                                                                                                                                                                                |
| ---------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `empty_page`                 | `blocking` | La page n'a aucune section, elle s'affiche vide                                                                                                                                                     |
| `order_form_without_product` | `blocking` | Une section prend des commandes, mais ni la page ni la section ne désigne un produit : les commandes seraient enregistrées à 0 DA                                                                   |
| `section_product_not_found`  | `blocking` | Une section pointe vers un produit qui n'est pas dans votre boutique                                                                                                                                |
| `no_order_form`              | `warning`  | Rien sur la page ne peut prendre une commande                                                                                                                                                       |
| `multiple_order_forms`       | `warning`  | Plus d'un `order_form` ou `order_button` s'affiche                                                                                                                                                  |
| `variants_need_page_product` | `warning`  | Une section désigne un produit à variantes alors que la page n'a pas de produit. Les sélecteurs de variantes ne s'affichent qu'à partir du produit de la page : définissez `product_id` sur la page |

La publication est refusée tant qu'un problème bloquant subsiste (voir plus bas).

## `POST /v1/landing-pages/{id}/publish`[​](#post-v1landing-pagesidpublish "Lien direct vers post-v1landing-pagesidpublish")

Raccourci : passer le status à `active`. Équivaut à `PATCH ... { status: "active" }`, et refusé de la même façon : tant que `GET /v1/landing-pages/{id}/check` signale un problème bloquant, l'appel répond `422 page_not_publishable` et l'erreur porte la liste `problems`. Les modifications qui n'envoient pas `status` ne passent pas par ce contrôle, même sur une page en ligne.

```
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`[​](#post-v1landing-pagesgenerate "Lien direct vers post-v1landing-pagesgenerate")

Construit une landing page à partir d'un de vos produits avec l'IA. Répond `202` tout de suite avec une tâche à suivre ; une génération prend environ deux minutes. **Auth :** le scope `ai:generate`. Les clés créées dans le tableau de bord ou avec `POST /v1/keys` et les applications tierces ne le portent pas et reçoivent `403 forbidden` ; les connexions Claude, ChatGPT et DZBuild Copilot le portent. **Nécessite `Idempotency-Key`.**

| Champ         | Type                 | Requis | Notes                                                             |
| ------------- | -------------------- | ------ | ----------------------------------------------------------------- |
| `title`       | string               | ✅     | Au moins 3 caractères, coupé à 255                                |
| `product_id`  | int                  | ✅     | Un produit actif de votre boutique avec au moins une image        |
| `description` | string               |        | Brief pour la page, coupé à 2000 caractères                       |
| `language`    | `ar` \| `fr` \| `en` |        | Défaut `ar`                                                       |
| `size`        | `medium` \| `tall`   |        | Défaut `medium`. `medium` coûte 20 crédits IA, `tall` en coûte 45 |

La réponse porte `task_id`, `size`, `credits_charged`, `eta_seconds` et `poll`. Les crédits sont débités au lancement et rendus si la génération échoue ou dépasse le délai. Erreurs : `400 bad_request` (titre manquant ou de moins de 3 caractères), `402 quota_exceeded` (crédits IA insuffisants), `403 limit_reached` (limite de landing pages du plan, avec `limit` et `current`), `409 already_processing` (une seule génération à la fois par boutique), `422 product_required`, `product_not_found` ou `product_has_no_image`, `429 rate_limited` ou `too_many_concurrent`, `503 provider_unavailable` (aucun crédit débité).

Suivez `GET /v1/landing-pages/generate/{task_id}` avec `landing_pages:read`. La réponse porte `task_id`, `status` (`processing` pendant la construction, puis `completed` ou `failed`), `landing_page_id` dès que la page existe, `current_step` et `error`. Une génération encore en cours après 10 minutes passe à `failed` avec l'erreur `timeout` à la consultation suivante, et ses crédits sont rendus.

## `DELETE /v1/landing-pages/{id}`[​](#delete-v1landing-pagesid "Lien direct vers delete-v1landing-pagesid")

Suppression dure. Les sections de la page sont supprimées avec elle.

```
curl -X DELETE 'https://api.dzbuild.app/v1/landing-pages/42' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: del-42"
```

Réponse : `{ "data": { "deleted": true, "id": 42 } }`.
