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

# Build a platform

> Model each customer as a workspace, connect their WhatsApp number, scope API keys, and route webhooks back to the right tenant.

If you're building WhatsApp into your own product, each customer brings their own Business Account. LazyApp's tenant boundary for this is the **platform customer**: one isolated workspace per customer, their connection inside it, and your database holding the mapping via `external_id`.

This page covers the core model. Once it's in place, the recipes show how to build features on top without fighting rate limits:

> **[Tenant messaging](https://docs.lazyapp.in/platform/messaging.md)**
>
> Retry-safe sends for every customer: idempotency, templates, scheduling

> **[Tenant usage dashboards](https://docs.lazyapp.in/platform/usage.md)**
>
> Show each customer their consumption and cost from the usage API

> **[Tenant inbox](https://docs.lazyapp.in/platform/inbox.md)**
>
> Conversations per customer: webhook-first, replies, embeds

Requests flow one way, events flow back — and your database is what connects the two:

```mermaid theme={null}
flowchart LR
  subgraph app ["Your app"]
    DB[("Your database<br/>customer acct_8812<br/>cus_… + connection map")]
  end
  subgraph lazyapp ["LazyApp"]
    CUS["Platform customer Acme"]
    WA["WhatsApp +91…"]
  end
  DB -->|"X-LazyApp-Customer or scoped key"| CUS
  CUS --> WA
  WA -.->|"webhooks carry customer envelope"| DB
```

Your own workspace stays separate. Create one platform customer per of *their* customers alongside it.

> [!NOTE]
>
> **Building a product, or reselling LazyApp?** This section is for platform teams: you call `/v1/platform/*` from your own app, and your platform wallet pays ₹999 a month per connected customer number. If you want to put your brand on the LazyApp console and charge businesses your own monthly price, without writing code, see [Resellers](https://docs.lazyapp.in/platform/resellers.md).

***

## Step 1: Create a customer per tenant

When a customer signs up in your app, create their LazyApp customer and store the returned `id` (or just keep using your own `external_id` — LazyApp accepts either).

```bash cURL theme={null}
curl https://lazyapp.in/v1/platform/customers \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Retail",
    "external_id": "acct_8812"
  }'
```

```js Node theme={null}
const response = await fetch('https://lazyapp.in/v1/platform/customers', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LAZYAPP_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Acme Retail',
    external_id: 'acct_8812',
  }),
});

const customer = await response.json();
// Store customer.id (cus_…) — or keep looking them up by external_id
```

```python Python theme={null}
customer = requests.post(
    "https://lazyapp.in/v1/platform/customers",
    headers={"Authorization": f"Bearer {os.environ['LAZYAPP_API_KEY']}"},
    json={
        "name": "Acme Retail",
        "external_id": "acct_8812",
    },
).json()
# Store customer["id"] — or keep looking them up by external_id
```

`external_id` is your own account id, so you never need a mapping table. Look them up later with `GET /v1/platform/customers?external_id=acct_8812`.

Creating a customer costs nothing. What you pay LazyApp is ₹999 a month for each customer number that connects, covered in [How platform billing works](#how-platform-billing-works).

> [!NOTE]
>
> `markup_bps`, `pricing_mode`, `category_markups` and `platform_fee` are no longer stored or returned. LazyApp adds nothing to Meta's rates, so there is no per-customer markup to configure. A request that still sends them succeeds, and the values are ignored.

See [Create a customer](https://docs.lazyapp.in/api-reference/platform/create-a-customer.md) for all fields.

***

## Step 2: Let them connect WhatsApp

Create a [setup link](https://docs.lazyapp.in/platform/setup-links.md) so they connect through Meta's Embedded Signup. You never handle their credentials.

```bash theme={null}
curl https://lazyapp.in/v1/platform/setup-links \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "cus_9f8b1c2d",
    "expires_in_days": 7,
    "redirect_url": "https://yourapp.example/settings/whatsapp?connected=1"
  }'
# → { "url": "https://lazyapp.in/connect/set_…", … }
```

Hand `url` to your customer — email it, or put it behind a **Connect WhatsApp** button. The host is LazyApp’s domain unless you set a public connect host under **Branding**. That value is an origin (`https://connect.yourapp.example`). Point it at LazyApp so `/connect/{token}` resolves, and the page shows your brand name, logo, and accent. Meta’s own signup screen still says Facebook.

Subscribe to [`customer.connected`](https://docs.lazyapp.in/webhooks/events#platform) to learn about new connections server-side. Platform deliveries include a `customer` envelope with your `external_id`:

```json theme={null}
{
  "id": "evt_…",
  "type": "customer.connected",
  "created_at": "2026-09-01T12:00:00+00:00",
  "customer": {
    "id": "cus_9f8b1c2d",
    "external_id": "acct_8812",
    "name": "Acme Retail"
  },
  "data": { }
}
```

Also handle `setup_link.failed` and `connection.attention_required` / `connection.disconnected` so you can prompt a reconnect.

The whole onboarding flow, end to end:

```mermaid theme={null}
sequenceDiagram
  participant C as Customer
  participant A as Your app
  participant L as LazyApp
  participant M as Meta
  A->>L: POST /v1/platform/customers
  L-->>A: cus_… — store id / keep external_id
  C->>A: clicks Connect WhatsApp
  A->>L: POST /v1/platform/setup-links
  L-->>A: url
  A->>C: open setup link
  C->>M: Embedded Signup
  M->>C: redirect to redirect_url
  L-->>A: webhook customer.connected (customer envelope)
  Note over A: mark tenant ready to send
```

Do not trust the redirect alone — it is UX only. Confirm from the webhook. Details in [setup links](https://docs.lazyapp.in/platform/setup-links.md).

***

## Step 3: Send on their behalf

Two patterns. Pick one:

| Pattern | How | When |
| - | - | - |
| Customer-scoped key | Mint once, store the key, call `/v1` as that customer | Dedicated backend per tenant, or long-lived agent |
| Platform key + header | One key, pass `X-LazyApp-Customer` on every request | Single integration that fans out to many tenants |

### Mint a scoped key

```bash theme={null}
curl https://lazyapp.in/v1/platform/customers/cus_9f8b1c2d/api-keys \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme server",
    "scopes": ["messages:send", "messages:read"]
  }'
```

That key can only see that customer's workspace. Omit `scopes` to grant the defaults for a customer-scoped key.

### Or act with the platform key

```bash theme={null}
curl https://lazyapp.in/v1/messages \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "X-LazyApp-Customer: acct_8812" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
    "type": "template",
    "to": "+919812345670",
    "template": { "name": "order_shipped", "language": "en" }
  }'
```

`X-LazyApp-Customer` accepts `public_id`, `uuid`, or `external_id`. The key needs `customers:act` (send only) or `customers:write` (full management). See [Authentication](https://docs.lazyapp.in/authentication.md#acting-for-a-platform-customer).

The messaging recipe covers idempotency, templates, and scheduling: [Tenant messaging](https://docs.lazyapp.in/platform/messaging.md).

***

## Step 4: Route webhooks back to the right customer

Webhook endpoints are configured once on your platform workspace, not per customer. Register one endpoint and route internally.

Every event that originated in a customer's workspace is also delivered to your platform endpoints, with a `customer` envelope:

| Events | Tenant key | Look it up in |
| - | - | - |
| Message, template, OTP, contact, connection, … | `customer.external_id` (or `customer.id`) | Your customer record |
| `customer.connected` / `setup_link.failed` | Same envelope | Mark onboarding complete / failed |
| `customer.usage_alert` | Same envelope | Notify that tenant about spend |

See [Webhooks](https://docs.lazyapp.in/webhooks/overview.md) for delivery, retries, and signature verification, and [Tenant inbox](https://docs.lazyapp.in/platform/inbox.md) for a full handler sketch.

***

## Rate limits with many tenants

One platform API key is enough for your whole integration. LazyApp limits requests **per key**, not per customer — see [rate limits](https://docs.lazyapp.in/reference/rate-limits.md) for the ceiling and the headers that report it. Meta separately caps how many unique contacts a number may message in 24 hours; that limit is per connection, reported on [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities).

That budget serves all your tenants together, which has three practical consequences:

* **Prefer webhooks over polling.** Most per-tenant "is there anything new?" traffic disappears when webhooks push events to you.
* **Run background work as one smooth queue.** A sync worker with a small concurrency cap uses the budget steadily. Per-tenant cron jobs that all fire at `:00` create bursts.
* **Fairness between tenants is yours to enforce.** LazyApp limits your key, not your customers — if one tenant triggers a huge burst, queue it so it cannot starve everyone else.

When you get a `429`, back off and retry with the same [Idempotency-Key](https://docs.lazyapp.in/concepts/idempotency.md). Handle `402` distinctly. It is a billing state, not a transient error: the customer is suspended, its spending limit is reached, or a wallet that has to pay for the send is empty. Alert someone; retrying will not help.

***

## Monitor connection health

WhatsApp credentials fail: Meta reviews, quality issues, or a customer reconnects elsewhere. Build the reconnect loop up front:

1. Subscribe to `connection.attention_required` and `connection.disconnected` — platform deliveries include the `customer` envelope.
2. Prompt that customer to reconnect with a new [setup link](https://docs.lazyapp.in/platform/setup-links.md) (`allowed_connection_type: "reconnect"`).
3. As a safety net, periodically call [`GET /v1/capabilities`](https://docs.lazyapp.in/concepts/capabilities) with `X-LazyApp-Customer` (or a scoped key) to catch anything a webhook missed.

***

## How platform billing works

A platform team pays LazyApp **₹999 a month for each connected customer number**, debited from your platform wallet. Creating customers and minting keys cost nothing, and so do sends on customer-paid numbers. Add money to the wallet from **Billing** in your platform workspace; see [Wallets](https://docs.lazyapp.in/billing/wallets.md).

In return your team is on the Platform plan: every feature, including the API, webhooks, automations, Flows, payments and catalog, with no caps on members, numbers or broadcast sends. Each managed customer workspace inherits your plan, so a customer never hits a [plan limit](https://docs.lazyapp.in/billing/pricing-and-tiers.md#feature-gating) that your team does not. The console marks the wallet low once it holds less than one month of your number cost.

### Who pays Meta

LazyApp adds 0% to Meta's message rates, and platform customers carry no markup. Who pays Meta for a customer's messages is set once per WhatsApp Business Account, by `meta_billing_mode` on the [setup link](https://docs.lazyapp.in/platform/setup-links.md#options):

| `meta_billing_mode` | Who pays Meta | What your wallet pays per message |
| - | - | - |
| `customer_managed` (default) | Meta bills the business directly, on its own Meta payment method | Nothing. `total_charge` is `0` |
| `partner_managed` | LazyApp pays Meta, using a LazyApp credit line | Meta's rate, plus 5% on a non-USD account and 5% for messages not sent through LazyApp |

Customer-paid numbers are the default because they keep you out of the money flow: Meta invoices your customer, and your wallet only covers the ₹999 per number. See [Meta message billing](https://docs.lazyapp.in/billing/meta-message-billing.md) for how Meta charges.

### Billing modes

`billing_mode` decides whose wallet takes a charge when there is one — that is, only for messages LazyApp pays Meta for:

| Mode | Meaning |
| - | - |
| `inherit` | Default. Charges debit your platform wallet |
| `prepaid` | Charges debit that customer's wallet (`wallet_balance_cents`). Sending is refused with `402` while it is at zero — even on a customer-paid number, so keep those on `inherit` |
| `postpaid` | Usage is recorded. Neither wallet is debited |

Set `spending_limit_cents` to cap what a customer's sends may cost in a month, and `usage_alert_cents` to receive `customer.usage_alert` before they reach it. Both measure `total_charge`, so on a customer-paid number they never trigger. Read consumption with [`GET /v1/platform/customers/{customer}/usage`](https://docs.lazyapp.in/platform/usage).

You bill your own customers however you like, outside LazyApp.

***

## Offboarding a customer

| Action | Effect |
| - | - |
| `POST …/suspend` | Blocks sending immediately, keeps everything. Reversible. |
| `POST …/archive` | Retires a workspace you may want back. Reversible. |
| `DELETE …` | Erases the workspace. Irreversible — prefer archive. |
| `POST …/restore` | Undoes suspend or archive. |

***

## Embedding UI

Rather than rebuilding an inbox, drop LazyApp components into your product with short-lived, origin-locked tokens. See [embedded components](https://docs.lazyapp.in/platform/embedded-components.md).

***

## Related

* [Tenant messaging](https://docs.lazyapp.in/platform/messaging.md) — retry-safe sends per customer
* [Tenant usage](https://docs.lazyapp.in/platform/usage.md) — per-customer consumption dashboards
* [Resellers](https://docs.lazyapp.in/platform/resellers.md) — sell LazyApp under your own brand
* [Plans and limits](https://docs.lazyapp.in/billing/pricing-and-tiers.md) — what each plan includes
* [Tenant inbox](https://docs.lazyapp.in/platform/inbox.md) — webhook-first conversations
* [Setup links](https://docs.lazyapp.in/platform/setup-links.md) — Embedded Signup
* [Idempotency](https://docs.lazyapp.in/concepts/idempotency.md) — safe retries
* [Rate limits](https://docs.lazyapp.in/reference/rate-limits.md) — windows and backoff
* [Webhooks](https://docs.lazyapp.in/webhooks/overview.md) — delivery and signatures


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.