# Manage customers with sub-organizations (/docs/api/guides/manage-customers-with-sub-organizations)



## What you'll build [#what-youll-build]

A full customer lifecycle on the [sub-organization](/docs/api/concepts/organizations) model: you'll create a **child org** for a new customer, provision a project inside it using your parent key and the `X-Layers-Organization` header, check the child's credit balance, flip the kill switch when you need to pause them, and offboard them cleanly when they churn.

The running example is **Quinn's Coffee CRM** onboarding their customer **Acme Coffee** as an isolated tenant.

<Callout>
  This is the control-plane flow. If you only have a handful of customers, the flat model ([one project per customer](/docs/api/concepts/projects) directly under your org) is simpler and unchanged — reach for sub-orgs when you want hard isolation and atomic offboarding.
</Callout>

## Before you start [#before-you-start]

1. **A live parent key with the `org:admin` scope.** This is the [deny-by-default](/docs/api/concepts/api-keys#scopes) control-plane scope — it must be granted explicitly; partner-tier keys don't get it for free. Ask your Layers contact to mint one. Treat it like a Stripe secret key — server-side only.
2. **Your parent org id** (from [`GET /v1/whoami`](/docs/api/reference/organizations/whoami)). Every child you create nests under it.

<Callout type="info">
  Two ways to drive a child: your parent key + the `X-Layers-Organization` header (used throughout this guide), or a **dedicated child API key** you mint per customer so their own integration authenticates directly. This guide shows both — the header for your own backend, and minting a child key in the [Issue a key](#issue-an-api-key-for-the-customer) step.
</Callout>

## How it works (in 30 seconds) [#how-it-works-in-30-seconds]

1. Create a child org per customer — it's an isolation boundary with its own wallet and audit trail.
2. To do anything *inside* a child (create projects, generate content, read credits), send your parent key plus `X-Layers-Organization: org_<child-id>`. Every existing endpoint works unchanged.
3. Fund the child by allocating credits from your parent wallet, set its spend limits (cap + auto-refill), and (optionally) issue it a dedicated API key.
4. Subscribe one parent webhook endpoint to every child's events (the firehose).
5. Suspend a child to instantly cut off its access; resume to restore it.
6. Archive a child to offboard — terminal, revokes its keys, reclaims unspent credits, one call.

<Steps>
  <Step>
    ## Create the customer's org [#create-the-customers-org]

    <Endpoint method="POST" path="/v1/organizations" scope="org:admin" phase="1" />

    Stash your own customer id in `metadata` so later lookups don't need Layers ids.

    ```bash
    curl https://api.layers.com/v1/organizations \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Acme Coffee",
        "metadata": { "externalId": "acme-coffee", "plan": "growth" }
      }'
    ```

    ```json
    {
      "id": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
      "parentOrganizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
      "name": "Acme Coffee",
      "status": "active",
      "metadata": { "externalId": "acme-coffee", "plan": "growth" },
      "billingEmail": null,
      "createdAt": "2026-06-01T14:30:00.000000+00:00",
      "updatedAt": "2026-06-01T14:30:00.000000+00:00"
    }
    ```

    Persist `id` (`org_d4e5f6a7-…`) against your customer record — it's the handle for everything below.
  </Step>

  <Step>
    ## Work inside the child org [#work-inside-the-child-org]

    To provision the customer's first [project](/docs/api/concepts/projects), call the normal `POST /v1/projects` endpoint — but add the `X-Layers-Organization` header so the project is created *in the child*, not in your parent org:

    ```bash
    curl https://api.layers.com/v1/projects \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "X-Layers-Organization: org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Acme Coffee", "timezone": "America/Los_Angeles" }'
    ```

    The returned project's `organizationId` is the **child** org. Every project-scoped call you make with that header resolves inside the child; the project is invisible to your other customers. The header is honored only because your key holds `org:admin` — a non-admin key sending it would be ignored, and a child that isn't yours returns `404`.

    <Callout type="info">
      Anything you can do in your own org, you can do inside a child by adding the header: generate content, connect social accounts, schedule posts. No new endpoints to learn.
    </Callout>
  </Step>

  <Step>
    ## Check the customer's usage [#check-the-customers-usage]

    Read the child's wallet and project count directly:

    ```bash
    curl https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80 \
      -H "Authorization: Bearer $PARENT_KEY"
    ```

    ```json
    {
      "id": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
      "name": "Acme Coffee",
      "status": "active",
      "summary": {
        "projectCount": 1,
        "balance": 0,
        "available": 0,
        "creditConfig": {
          "monthlyCreditCap": null,
          "refillThreshold": null,
          "refillAmount": null,
          "autoRefillEnabled": false
        }
      }
    }
    ```

    A new child starts with an empty, isolated wallet — `balance` is the gross credit total and `available` is what's spendable right now (net of credits reserved for in-flight generations). `creditConfig` starts all-null (no cap, no auto-refill). Fund it and set its limits in the next steps.
  </Step>

  <Step>
    ## Fund the customer's wallet [#fund-the-customers-wallet]

    <Endpoint method="POST" path="/v1/organizations/{orgId}/credits/allocate" scope="org:admin" phase="1" />

    Move credits from your parent wallet down to the child. The `Idempotency-Key` header is **required** here — a request without it is rejected with `400 IDEMPOTENCY_REQUIRED`, and it's what stops a retry from double-funding.

    ```bash
    curl -X POST https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80/credits/allocate \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{ "credits": 5000 }'
    ```

    ```json
    {
      "id": "txn_7f3a2b1c-9d8e-4f0a-b1c2-3d4e5f6a7b8c",
      "organizationId": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
      "allocated": 5000,
      "balance": 5000,
      "available": 5000,
      "description": null,
      "metadata": {},
      "created": "2026-06-03T18:14:02.187000+00:00"
    }
    ```

    The `id` is the transfer id (`txn_`-prefixed) — your reconciliation handle, also surfaced on both ledgers' `allocation` events as `metadata.transferId`. The credits come out of your parent balance and into the child's. If your wallet is short you get `402 BILLING_EXHAUSTED`; allocating into an already-archived child returns `409 CONFLICT`. The transfer shows up on the child's [credit ledger](/docs/api/reference/organizations/credits/list-events) as an `allocation` event — and on your own ledger as the matching debit. Read the child's wallet any time with [`GET …/credits`](/docs/api/reference/organizations/credits/get-credits).
  </Step>

  <Step>
    ## Set the customer's spend limits [#set-the-customers-spend-limits]

    <Endpoint method="PATCH" path="/v1/organizations/{orgId}/credit-config" scope="org:admin" phase="1" />

    Rather than top a customer up by hand every time, set a monthly **cap** and an **auto-refill** rule once. Here: cap Acme at 5,000 credits/month, and auto-refill 2,000 whenever their spendable balance drops below 1,000.

    ```bash
    curl -X PATCH https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80/credit-config \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "monthlyCreditCap": 5000,
        "refillThreshold": 1000,
        "refillAmount": 2000
      }'
    ```

    ```json
    {
      "organizationId": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
      "config": {
        "monthlyCreditCap": 5000,
        "refillThreshold": 1000,
        "refillAmount": 2000,
        "autoRefillEnabled": true
      },
      "balance": 5000,
      "available": 5000
    }
    ```

    All three are in **credits** and nullable. Auto-refill is **both-or-neither** — set `refillThreshold` and `refillAmount` together (or clear both); a one-sided change is rejected with `422` (`details.code: REFILL_REQUIRES_THRESHOLD_AND_AMOUNT`). Once set, Acme tops itself up from your parent wallet and can't outspend the cap — an over-cap generation gets `402 BILLING_EXHAUSTED`. See the [Credits concept](/docs/api/concepts/credits#credit-config--caps-and-auto-refill) for the full behavior.
  </Step>

  <Step>
    ## Issue an API key for the customer [#issue-an-api-key-for-the-customer]

    <Endpoint method="POST" path="/v1/organizations/{orgId}/api-keys" scope="org:admin" phase="1" />

    If Acme's own integration should authenticate directly — instead of you proxying every call with the header — mint a [child API key](/docs/api/concepts/api-keys#child-keys-for-sub-organizations) scoped to their org. Grant only the scopes they need; they must be a subset of your parent key's scopes, and `org:admin` can never be delegated.

    ```bash
    curl -X POST https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80/api-keys \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{ "name": "acme-integration", "scopes": ["content:read", "content:write"] }'
    ```

    ```json
    {
      "apiKey": {
        "id": "key_c2037bb9-354d-4662-96b7-97a28ad6b6e1",
        "organizationId": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
        "name": "acme-integration",
        "prefix": "lp_live_ABCDEFGHJKMNPQRS",
        "scopes": ["content:read", "content:write"],
        "status": "active"
      },
      "secret": "lp_live_...",
      "warning": "Store this secret now. It cannot be retrieved again. Rotate the key if it's lost."
    }
    ```

    The `secret` is shown **once** — hand it to Acme through a secure channel and store nothing on your side. When it's time to roll it, [rotate](/docs/api/reference/organizations/api-keys/rotate) gives them a new secret while the old one keeps working for a 24-hour grace window.
  </Step>

  <Step>
    ## Watch every customer's events (the firehose) [#watch-every-customers-events-the-firehose]

    <Endpoint method="POST" path="/v1/webhook-endpoints" scope="org:admin" phase="1" />

    Instead of registering a webhook endpoint per customer, register **one** parent endpoint with `scope: "all_children"` — it receives every child's events alongside your own. Each delivery carries `data.organizationId`, so you route it to the right customer.

    ```bash
    curl -X POST https://api.layers.com/v1/webhook-endpoints \
      -H "Authorization: Bearer $PARENT_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://quinns-crm.example.com/layers/webhooks",
        "events": ["content.generated", "post.published"],
        "scope": "all_children"
      }'
    ```

    Setting `scope: "all_children"` requires `org:admin` (a non-admin key gets `403 FORBIDDEN_SCOPE`); the default is `own`. Every payload's `data.organizationId` tells you which child — for Acme, `org_d4e5f6a7-…`. See [Webhooks → delivery scope](/docs/api/operational/webhooks#delivery-scope-own-vs-the-firehose).
  </Step>

  <Step>
    ## Pause a customer (kill switch) [#pause-a-customer-kill-switch]

    Need to stop a customer immediately — non-payment, abuse, a support hold? [Suspend](/docs/api/reference/organizations/suspend-organization) the org. The child's own access is cut instantly; your parent key keeps working so you can investigate and resume.

    ```bash
    # Suspend
    curl -X POST https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80/suspend \
      -H "Authorization: Bearer $PARENT_KEY"

    # Later: lift it
    curl -X POST https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80/resume \
      -H "Authorization: Bearer $PARENT_KEY"
    ```

    Both are idempotent — suspending a suspended org (or resuming an active one) is a safe no-op.
  </Step>

  <Step>
    ## Offboard the customer [#offboard-the-customer]

    When a customer churns, [archive](/docs/api/reference/organizations/archive-organization) their org. One call revokes the child's keys, reclaims its unspent credits to your parent wallet, and moves it to the terminal `archived` state — no lingering tokens, no stranded balance, no entangled history.

    ```bash
    curl -X DELETE https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80 \
      -H "Authorization: Bearer $PARENT_KEY"
    ```

    ```json
    {
      "id": "org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80",
      "status": "archived",
      "archivedAt": "2026-06-02T13:00:00.000000+00:00",
      "reclaimedCredits": 3200,
      "revokedApiKeys": 2
    }
    ```

    `reclaimedCredits` is the child's unspent (non-reserved) balance returned to your parent wallet; `revokedApiKeys` counts the child keys killed (here: Acme's integration key plus any you minted). Archival is **terminal** — if Acme comes back, create a fresh child org.
  </Step>
</Steps>

## See also [#see-also]

* [Organizations concept](/docs/api/concepts/organizations) — the parent/child model and lifecycle.
* [Authentication → acting on behalf of a child](/docs/api/getting-started/authentication#acting-on-behalf-of-a-child-org) — the `X-Layers-Organization` header rules.
* [API keys → child keys](/docs/api/concepts/api-keys#child-keys-for-sub-organizations) — scope subsetting, the `org:admin` rule, rotation grace.
* [Allocate credits](/docs/api/reference/organizations/credits/allocate), [credit-config](/docs/api/reference/organizations/credit-config) (cap + auto-refill), and [child wallet](/docs/api/reference/organizations/credits/get-credits) — the per-child credit surface.
* [Credits concept](/docs/api/concepts/credits) — balance vs available, allocation, caps, auto-refill.
* [Provision and fund a customer](/docs/api/guides/provision-and-fund-a-customer) — the six-call linear quickstart version of this flow.
* [Organization reference](/docs/api/reference/organizations/create-organization) — every endpoint in detail.
