Layers
Partner APIGetting started

Authentication

API keys, header precedence, rotation, the kill switch, and rate-limit signals.

View as Markdown

The Partner API authenticates with one thing: an API key bound to your Layers organization. Every request carries it. Everything else - what you can do, how fast, who you can do it on behalf of - is read from the key on the server.

Key shape

lp_...

Keys are opaque production credentials. Do not parse structure from the string or branch on prefixes in your integration.

Treat the whole string as a secret. If an API response includes apiKeyId, that id is safe to log; the full key never is.

If a key leaks, hit the kill switch (below) immediately, then rotate.

Where to put the header

Send the API key as a Bearer token in the Authorization header on every request:

Authorization: Bearer lp_...

This is the only accepted auth form. Requests without it return 401 UNAUTHENTICATED.

curl https://api.layers.com/v1/whoami \
  -H "Authorization: Bearer $LAYERS_API_KEY"
await fetch("https://api.layers.com/v1/whoami", {
  headers: { "Authorization": `Bearer ${process.env.LAYERS_API_KEY}` },
});
import axios from "axios";

const layers = axios.create({
  baseURL: "https://api.layers.com",
  headers: { "Authorization": `Bearer ${process.env.LAYERS_API_KEY}` },
});

await layers.get("/v1/whoami");
import os, requests

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ[\'LAYERS_API_KEY\']}"

session.get("https://api.layers.com/v1/whoami")

Scopes

Scope enforcement is deny-by-default. Every key is gated against the scope declared on each endpoint reference page — a mismatch returns 403 FORBIDDEN_SCOPE. A key with an empty / missing scopes list grants nothing: every gated route returns 403. Keys minted before enforcement landed were migrated to the explicit * full-access sentinel (covers every data scope — see below), so they keep working; every new key must be minted with at least one scope. /v1/whoami returns the key's granted scopes (["*"] for a backfilled full-access key, the explicit list otherwise).

Every key carries a list of scopes. A request that hits the wrong scope gets 403 FORBIDDEN_SCOPE back - no retry helps; the key needs to be re-issued with the right scope set. The response body's error.details.requiredScope names the missing scope so you can ask the right team for the right grant.

ScopeLets you
projects:read / projects:writeList, read, create, patch, archive projects.
ingest:writeKick off GitHub, website, and App Store ingest jobs.
content:read / content:write / content:approveRead containers, generate, approve or reject.
social:read / social:writeList connected accounts; create OAuth URLs; revoke.
publish:read / publish:writeList + read scheduled posts; schedule, publish, reschedule, cancel.
events:read (optional +pii sub-scope)Read the SDK event stream. PII fields are redacted unless +pii is granted.
metrics:readRead organic and ads metrics, top performers, ads-content.
ads:read / ads:writeRead ad accounts, campaigns, adsets, ads; execute ad writes. Fine-grained ads:write:campaigns, ads:write:budgets, ads:write:creative, ads:write:lifecycle, ads:write:policy sub-scopes also exist (ads:write:* covers all of them).
influencers:read / influencers:writeList + read influencers; create, clone, patch.
leased:read / leased:writeList + read leased accounts; submit lease requests; release.
engagement:read / engagement:writeRead auto-pilot engagement config; patch it.
github:adminRegister a GitHub installation, list repos.
jobs:read / jobs:cancelPoll and cancel jobs.
credits:readRead org credits balance and per-format estimated costs.
org:adminCreate, suspend, archive, and migrate into child organizations; allocate credits; mint child API keys; act on a child via the X-Layers-Organization header. Non-delegable (see below).

org:admin is non-delegable. It gates the control plane — minting customer orgs, draining wallets, offboarding, minting child keys — so no wildcard confers it: a key holding * (full data access) still does not get org:admin. It must be granted explicitly, it is never auto-granted to any key tier, and a child key can never receive it (even from a parent that holds it).

The * sentinel covers every data scope in the table above but stops at the control plane: it never includes org:admin. Enterprise (partner-tier) keys may be provisioned with broad access by Layers, but the same rule holds — org:admin is only ever present when explicitly granted. Self-serve scope provisioning is planned; today an explicit scope list is set at key-creation time by an admin (or, for child keys, by a parent org:admin key — see below).

Per-customer scoping

One key. Many customers. Each end-customer is a project, and you pin which one a call is for via the path - /v1/projects/:projectId/... is implicitly scoped to a single project. The server checks the project belongs to your org and returns 404 NOT_FOUND otherwise (we don't leak existence with a 403).

If you need a belt-and-suspenders check against your own customer-external-id, read the project first via GET /v1/projects/:projectId and assert customerExternalId matches what your code thinks it should be before issuing follow-up calls.

Acting on behalf of a child org

If you run customers as sub-organizations, your parent key can operate inside a child org by sending the X-Layers-Organization header naming that child. This is the same pattern as Stripe's Stripe-Account header — every existing endpoint works unchanged; the server scopes the call to the child:

Authorization: Bearer lp_...           # your parent key
X-Layers-Organization: org_<child-id>  # the child to act as

The rules:

  • The header is honored only for keys that hold the org:admin scope. A key without it that sends the header is ignored — the request runs against the key's own org.
  • The target must be a direct child of the calling org. A child that doesn't exist, belongs to another partner, or isn't yours resolves to 404 NOT_FOUND (we don't leak existence with a 403).
  • Acting on behalf of a suspended child is allowed — so you can inspect and manage a paused customer. A terminal archived child returns 409 CONFLICT.
  • Without the header, an org:admin key behaves normally and acts on its own org.
# Read Customer A's credit balance, using your parent key
curl https://api.layers.com/v1/credits \
  -H "Authorization: Bearer $PARENT_KEY" \
  -H "X-Layers-Organization: org_cust_a"
await fetch("https://api.layers.com/v1/credits", {
  headers: {
    "Authorization": `Bearer ${process.env.PARENT_KEY}`,
    "X-Layers-Organization": "org_cust_a",
  },
});
import os, requests

requests.get(
    "https://api.layers.com/v1/credits",
    headers={
        "Authorization": f"Bearer {os.environ['PARENT_KEY']}",
        "X-Layers-Organization": "org_cust_a",
    },
)

Or mint a dedicated child key

The X-Layers-Organization header lets your parent key reach into a child. For a stronger isolation boundary — a separate credential per customer rather than one key spanning all of them — mint a child API key scoped to a single child org with your parent (org:admin) key. The customer's own integration then authenticates directly without ever touching the parent or its siblings. A child key can only carry scopes you already hold, and never org:admin.

Pick by who holds the credential: the header when your backend drives every customer with one parent key; a child key when the customer (or a per-customer service) authenticates on its own.

GET /v1/whoami tells you which kind of key you're holding: parentOrganizationId is null for a top-level / parent key and set (to the org_… parent) for a child key, and scopes lists the key's granted scopes as a flat string array.

Rotation

Keys don't expire on a schedule by default - rotate when you have a reason (employee left, leak suspected, regular hygiene cadence). The rotation pattern:

  1. Ask your Layers contact to create a second key with the same access.
  2. Roll it out across your services. Keep the old key live during the cutover.
  3. Confirm callers have stopped using the old key for a full deploy cycle, then revoke it.
  4. If you're nervous, kill-switch the old key first (instant) and only revoke after a soak.

The two-key-overlap window is the safest pattern. There is no way to "rotate the secret in place" - the old key is gone the moment it's revoked, and any in-flight request still using it gets 401 UNAUTHENTICATED.

Kill switch

If a key is exposed - committed to a public repo, leaked in a screenshot, anything - flip the kill switch first and ask questions later.

  • Per key. Layers can disable a single key. Every subsequent request returns 503 KILL_SWITCH immediately, no retry. Reads, writes, polls - all of it. Killed keys can be un-killed; revoked keys are gone.
  • Per organization. An org-wide kill cuts every key on the org at once. Useful if you don't know which key leaked.
  • Global. A platform-wide kill exists for incident response. You'll see 503 KILL_SWITCH across the board if it ever fires; check the support before paging us.

There is no programmatic kill-switch endpoint today - email or Slack your Layers contact.

Rate limits

Every key has a tier. standard is the default; higher tiers are provisioned by Layers for enterprise partners. Buckets are keyed per (api_key_id, endpoint_class) - a noisy generation endpoint won't starve your read traffic, but it can starve other writes on the same key.

TierTypical provisioning
standardDefault for all partner keys.
pilotGranted for early-integration partners with planned higher throughput.
partnerEnterprise tier for GA partners with SLAs.

Hit a limit and you get 429 RATE_LIMITED. The signals:

  • Retry-After header.
  • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers - bucket state.
  • X-RateLimit-Endpoint-Class: read-light | write-light | long-running - which bucket you hit.
  • X-RateLimit-Tier: standard - the tier in effect.
  • Body: { "error": { "code": "RATE_LIMITED", "requestId": "req_...", "details": { "endpointClass": "write-light", "retryAfterMs": 1240 } } }.

Honor Retry-After. See rate limits for the full bucket table and 429 envelope.

Best practices

  • Store the key in your secret manager. Never in source, never in env files committed to git, never in screenshots.
  • Use throwaway projects and read-only calls for CI smoke tests. Every key operates against production systems.
  • Create one key per integration, not one key per developer. Easier to rotate, easier to attribute usage.
  • Set up a dashboard on the 429 rate so a quiet drift toward your ceiling doesn't surprise you.
  • For idempotent retries on POSTs, see Common patterns → idempotency.

On this page