Authentication
API keys, header precedence, rotation, the kill switch, and rate-limit signals.
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.
| Scope | Lets you |
|---|---|
projects:read / projects:write | List, read, create, patch, archive projects. |
ingest:write | Kick off GitHub, website, and App Store ingest jobs. |
content:read / content:write / content:approve | Read containers, generate, approve or reject. |
social:read / social:write | List connected accounts; create OAuth URLs; revoke. |
publish:read / publish:write | List + 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:read | Read organic and ads metrics, top performers, ads-content. |
ads:read / ads:write | Read 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:write | List + read influencers; create, clone, patch. |
leased:read / leased:write | List + read leased accounts; submit lease requests; release. |
engagement:read / engagement:write | Read auto-pilot engagement config; patch it. |
github:admin | Register a GitHub installation, list repos. |
jobs:read / jobs:cancel | Poll and cancel jobs. |
credits:read | Read org credits balance and per-format estimated costs. |
org:admin | Create, 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 asThe rules:
- The header is honored only for keys that hold the
org:adminscope. 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 a403). - 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:adminkey 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:
- Ask your Layers contact to create a second key with the same access.
- Roll it out across your services. Keep the old key live during the cutover.
- Confirm callers have stopped using the old key for a full deploy cycle, then revoke it.
- 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_SWITCHimmediately, 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_SWITCHacross 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.
| Tier | Typical provisioning |
|---|---|
standard | Default for all partner keys. |
pilot | Granted for early-integration partners with planned higher throughput. |
partner | Enterprise tier for GA partners with SLAs. |
Hit a limit and you get 429 RATE_LIMITED. The signals:
Retry-Afterheader.X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Resetheaders - 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
429rate so a quiet drift toward your ceiling doesn't surprise you. - For idempotent retries on POSTs, see Common patterns → idempotency.
Getting started
Create a project, generate a piece of content, and (optionally) schedule it to a connected social account. Three calls, one coherent flow — Layers auto-creates the project's keyword bank and first influencer in the background.
Common patterns
The five patterns you'll hit on day one - idempotency, pagination, errors, async jobs, rate-limit headers.