> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Landing pages: build, publish, and attribute

> Build a no-code mobile landing page or link-in-bio microsite, publish it to a trackable short link, and read per-page visits and conversions attributed back to the sending message or campaign.

# Landing pages

The landing-page builder turns a campaign link destination into something you own: a mobile-first microsite assembled from blocks, published to a trackable short link, with every visit and conversion attributed back to the message and campaign that sent it. No code, no external hosting, no third-party page tool — the page, its short link, and its analytics all live on the same `/links` surface you already use.

For request and response schemas, see the [links API reference](/api-reference/links). For the click-quality pipeline the short link inherits, see [short links with click tracking](/guides/short-links-and-click-tracking).

## 1. What the builder is for

SMS and WhatsApp bodies travel plain — a long URL at the end of a message converts worse than a focused destination page, and every character costs segments. The builder gives a campaign a rich destination without standing up infrastructure:

* A **mobile landing page** for a campaign — headline, offer, opt-in form, coupon.
* A **link-in-bio microsite** published once, then dropped into any SMS, WhatsApp, or RCS message.
* A **conversion surface** — a recipient's opt-in submit, coupon claim, or CTA click is recorded as a conversion event attributed to the exact message they came from.

Pages render server-side as a single self-contained HTML document with no client-side JavaScript, and every operator-supplied string is HTML-escaped, so a page definition cannot inject markup or script into a recipient's browser.

## 2. Create a page in the dashboard

Open **Messages → Landing Pages** in the dashboard and click **New landing page**. Give the page a title, optionally a slug (a short, URL-safe label; when omitted, the slug is derived from the title), and define the page content. Every new page starts in **draft** — it has no public URL yet and nothing is visible to recipients.

Drafts are safe to save and iterate on: the block list can be empty while you assemble the page, and nothing about a draft is reachable from outside your tenant.

The same create operation is available over the API:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/links/landing-pages \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Summer sale",
    "slug": "summer-sale",
    "content": {
      "theme": { "accentColor": "#325FEC" },
      "blocks": [
        {
          "type": "hero",
          "heading": "25% off this weekend",
          "subheading": "For our SMS subscribers only.",
          "ctaLabel": "Shop now",
          "ctaUrl": "https://example.com/sale"
        },
        {
          "type": "opt_in_form",
          "heading": "Get the early-access code",
          "fields": ["email"],
          "submitLabel": "Sign up",
          "consentText": "By signing up you agree to receive messages."
        }
      ]
    }
  }'
```

Response:

```json theme={null}
{
  "data": {
    "id": "landing-page_01J6Q03HEY8ZXM4NPTKVWB2DRS",
    "slug": "summer-sale",
    "title": "Summer sale",
    "status": "draft",
    "short_url": null,
    "visits": 0,
    "conversions": 0
  },
  "meta": { "requestId": "req_01J6Q03HJFQGJ9K8R1Q6P2M2RE" }
}
```

If the slug you asked for is already taken, it gets a short suffix so the create still succeeds — the response carries the resolved value.

Edit the page at any time with `PATCH /api/v1/links/landing-pages/:id`. Every field is optional; updates work for drafts and published pages alike, and a published page keeps its existing short link while its content changes. Set `"status": "archived"` on the same call to retire a page without deleting its history. List everything (optionally filtered by `status`) with `GET /api/v1/links/landing-pages`.

## 3. The block library and the content model

A page is a list of blocks. The builder's model stores exactly one **`theme`** (optional accent and background colors, hex literals like `#325FEC`) plus up to **50 blocks**, each one of four types:

| Block          | What it renders                                              | Key fields                                                |
| -------------- | ------------------------------------------------------------ | --------------------------------------------------------- |
| `hero`         | Headline, optional subheading, optional image and CTA button | `heading`, `subheading`, `imageUrl`, `ctaLabel`, `ctaUrl` |
| `product_grid` | A grid of 1–24 products with optional price, image, and link | `heading`, `products[]`                                   |
| `opt_in_form`  | A native form collecting 1–3 of `name`, `email`, `phone`     | `fields`, `submitLabel`, `consentText`                    |
| `coupon`       | A claimable coupon with a code and description               | `code`, `description`, `expiresAt`                        |

Two validation levels apply. On **create and update** the content is lenient — an empty block list is a legal work-in-progress draft, and a malformed block answers a `422` naming the field. At **publish** the strict level applies: the page must contain at least one block.

Dashboard cards show each page's live `visits` and `conversions` counters; the API row carries the same counts alongside `status` (`draft`, `published`, `archived`), `short_code`, and the campaign attribution.

## 4. Publish to a trackable short link

Publishing is one call:

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/links/landing-pages/landing-page_01J6Q03HEY8ZXM4NPTKVWB2DRS/publish \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

Response:

```json theme={null}
{
  "data": {
    "id": "landing-page_01J6Q03HEY8ZXM4NPTKVWB2DRS",
    "status": "published",
    "short_code": "mN7qR2",
    "short_url": "https://api.orbit.devotel.io/l/mN7qR2",
    "published_at": "2026-08-28T09:14:11.331Z"
  },
  "meta": { "requestId": "req_01J6Q04J2HGXR4C1Q8P3N2W0TE" }
}
```

Publishing mints a standard 6-character short code on the same shortener that powers `POST /api/v1/links` — so the page's `short_url` respects your [branded short domain](/guides/short-links-and-click-tracking) and inherits the full click pipeline: per-IP rate limiting, bot-vs-human click scoring, per-contact click history, and the `short_link.click` webhook. The short link redirects to the page itself, and any click attribution you have already configured for plain short links covers landing-page traffic with no extra wiring.

Publish is idempotent. Calling it again on a published page returns the existing short link — no new code is minted. Update the content and call publish again and the same short URL keeps working.

Drop the short URL into any message — SMS, WhatsApp, RCS — as the campaign destination. Because it is a standard tracked link, click-through reporting on the message plays against the same funnel you already read.

## 5. Analytics: visits and conversions per page

Every page aggregates two counters: a **visit** (a recipient opening the page) and a **conversion** (an opt-in form submit, a coupon claim, or a CTA click). Read the rollup over the API:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/links/landing-pages/landing-page_01J6Q03HEY8ZXM4NPTKVWB2DRS/analytics?top_limit=10" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

Response:

```json theme={null}
{
  "data": {
    "page": {
      "id": "landing-page_01J6Q03HEY8ZXM4NPTKVWB2DRS",
      "status": "published",
      "short_url": "https://api.orbit.devotel.io/l/mN7qR2",
      "visits": 1312,
      "conversions": 184
    },
    "analytics": {
      "totals": { "visits": 1312, "conversions": 184 },
      "by_campaign": [
        {
          "campaign_id": "campaign_01J6P9XA01",
          "campaign_name": "Summer Sale",
          "visits": 1240,
          "conversions": 177
        }
      ],
      "by_message": [
        { "message_id": "message_01J6PA2Z90", "visits": 612, "conversions": 89 }
      ],
      "recent_conversions": [
        {
          "id": "landing-page-event_01J6Q1BB22",
          "conversion_kind": "opt_in",
          "campaign_id": "campaign_01J6P9XA01",
          "message_id": "message_01J6PA2Z90",
          "contact_id": "contact_01H9XW3PTM8YJQKN2D4R5C6V7E",
          "value": "priya@example.com",
          "created_at": "2026-08-28T11:02:51.512Z"
        }
      ]
    }
  },
  "meta": { "requestId": "req_01J6Q05M7KZLQ9C2R4N8P1V3QA" }
}
```

In the dashboard, click **Analytics** on any page card to read the same totals plus the per-campaign breakdown. `top_limit` (1–50, default 10) bounds the `by_campaign`, `by_message`, and `recent_conversions` sections.

A conversion event records its kind, the sending campaign and message, the converting contact when known, and a value (the submitted email, the claimed coupon code). Aggregation is bounded — up to 50,000 recent events per page feed the rollup — so analytics stay fast on high-traffic pages.

## 6. Feeding campaign ROAS attribution

Attribution rides the events themselves: `{ campaign, message, contact }` is stamped on every recorded visit and conversion, so `by_campaign` and `by_message` answer "which sends drove page activity" without any join work on your side.

The per-page rollup slots into campaign ROAS reporting on the **engagement** rung of the funnel — a page visit is the downstream of the message's tracked click, and a conversion is the measurable action the campaign paid for. Send the page's short link through a campaign and the campaign's attribution panel picks up the same events alongside its tracked clicks and conversions. See [campaign ROAS and revenue attribution](/guides/campaign-roas-attribution) for how attributed revenue, send cost, and ROAS are computed.

## 7. Roles: who can create, publish, and delete

The write operations — create, update, publish, delete — are restricted to **owner**, **admin**, and **developer** roles, on both the API and the dashboard. Other roles, including viewers, can list pages and read analytics but cannot create, publish, edit, or delete them. A caller outside the write roles gets a `403` on write calls.

The dashboard page applies the same guard, so what you see matches what the API would let you do.

## 8. Troubleshooting

| Symptom                                                            | Cause and fix                                                                                                                                                                            |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `422` on publish, message about needing at least one content block | The page's block list is empty. Add at least one block via a content patch, then publish again.                                                                                          |
| `422` on create or update naming a block field                     | A block is malformed — check the block type and field limits in section 3. The page is not modified.                                                                                     |
| `404` on `:id` endpoints                                           | The id belongs to a different tenant, or the page was deleted.                                                                                                                           |
| `403` on create / update / publish / delete                        | The caller's role is outside owner / admin / developer. Issue write calls from an allowed role.                                                                                          |
| Short URL still points at old content after an edit                | It does not — edits go live under the same short URL. If a recipient still sees stale markup, the message you're testing with carried an older link; the short link itself is unchanged. |

Delete a page with `DELETE /api/v1/links/landing-pages/:id` (`204` on success) when it is truly done — its visit and conversion events are removed with it. To keep the history, archive the page instead via `PATCH` with `"status": "archived"`.
