Layers

Manage customers with sub-organizations

Give every customer an isolated child org — create one, work inside it with your parent key, suspend it as a kill switch, and offboard it in a single call.

View as Markdown

What you'll build

A full customer lifecycle on the sub-organization 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.

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

Before you start

  1. A live parent key with the org:admin scope. This is the deny-by-default 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). Every child you create nests under it.

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 step.

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.

Create the customer's org

POST/v1/organizations
Phase 1stableidempotent
Auth
Bearer
Scope
org:admin

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

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" }
  }'
{
  "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.

Work inside the child org

To provision the customer's first project, 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:

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.

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.

Check the customer's usage

Read the child's wallet and project count directly:

curl https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80 \
  -H "Authorization: Bearer $PARENT_KEY"
{
  "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.

Fund the customer's wallet

POST/v1/organizations/{orgId}/credits/allocate
Phase 1stableidempotent
Auth
Bearer
Scope
org:admin

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.

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 }'
{
  "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 as an allocation event — and on your own ledger as the matching debit. Read the child's wallet any time with GET …/credits.

Set the customer's spend limits

PATCH/v1/organizations/{orgId}/credit-config
Phase 1stableidempotent
Auth
Bearer
Scope
org:admin

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.

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
  }'
{
  "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 for the full behavior.

Issue an API key for the customer

POST/v1/organizations/{orgId}/api-keys
Phase 1stableidempotent
Auth
Bearer
Scope
org:admin

If Acme's own integration should authenticate directly — instead of you proxying every call with the header — mint a child API key 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.

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"] }'
{
  "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 gives them a new secret while the old one keeps working for a 24-hour grace window.

Watch every customer's events (the firehose)

POST/v1/webhook-endpoints
Phase 1stableidempotent
Auth
Bearer
Scope
org:admin

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.

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.

Pause a customer (kill switch)

Need to stop a customer immediately — non-payment, abuse, a support hold? Suspend the org. The child's own access is cut instantly; your parent key keeps working so you can investigate and resume.

# 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.

Offboard the customer

When a customer churns, archive 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.

curl -X DELETE https://api.layers.com/v1/organizations/org_d4e5f6a7-8b9c-4d0e-9f2a-3b4c5d6e7f80 \
  -H "Authorization: Bearer $PARENT_KEY"
{
  "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.

See also

On this page