API keys
Key format, scope taxonomy, rate-limit tiers, and revocation paths.
An API key authenticates every request you make and decides what that request is allowed to do. One key is scoped to one organization. It carries the rate-limit tier that caps throughput, the allowlist of returnUrl values the OAuth flows will accept, and the scope list that gates individual routes.
Key format
lp_<env>_<keyid>_<secret>The env segment is one of live or test:
lp_live_…— production. Real platform calls, real credit burn, real OAuth.lp_test_…— sandbox. Deterministic fakes, no credit burn, no real platform calls. Identical response envelopes and signed webhooks. See Sandbox for the full behavior contract.
The two are minted separately. Routes don't gate on env — the same scope set works on either — but the request envelope's behavior diverges as soon as a sandbox key is detected. Any prefix-vs-row env mismatch is rejected at auth time with 401 UNAUTHENTICATED.
Treat the full key as an opaque secret beyond the env segment. Layers may expose a shortened key id in responses and audit logs; that id is safe to log, but the full key is not.
The secret is shown exactly once - at the moment of creation. We can't recover it, display it again, or confirm you have the right one. Paste it into your secrets manager before you close the tab.
Sending the key
Send the key as Authorization: Bearer <key> on every request. This is the only accepted auth form.
GET /v1/whoami HTTP/1.1
Host: api.layers.com
Authorization: Bearer lp_...The first call any client should make is GET /v1/whoami - it resolves your key to the org it's bound to and lets you fail fast if anything is wrong. Response shape:
{
"organizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
"workspaceId": "org_2481fa5c-a404-44ed-a561-565392499abc",
"organizationName": "Acme Growth",
"scopes": [],
"parentOrganizationId": null,
"rateLimitTier": "standard",
"apiKeyId": "key_c2037bb9-354d-4662-96b7-97a28ad6b6e1"
}parentOrganizationId names the parent org a child key reports to (or null for a top-level key); scopes is the key's granted scopes as a flat string array. See whoami for the full field reference.
A 200 from /whoami is itself the "key is live and the org is in good standing" signal. If access is suspended every endpoint - including this one - returns 503 KILL_SWITCH with a request id; quote that id to your Layers contact.
Scopes
Scope enforcement is live and deny-by-default. A key is granted only the scopes minted onto it; calling an endpoint whose declared scope the key doesn't hold returns 403 FORBIDDEN_SCOPE. An empty scope list grants nothing — every new key must be minted with at least one scope (see Creating API keys). /v1/whoami returns whichever scopes were minted onto the key.
Every scope and the functionality it gates:
| Scope | Functionality |
|---|---|
projects:read | List and read projects. |
projects:write | Create and update projects. |
ingest:write | Trigger GitHub / website / App Store ingestion jobs. |
content:read | List and read generated content. |
content:write | Start content generation (slideshow / video / UGC remix). |
content:approve | Approve or reject generated content. |
social:read | List connected social accounts. |
social:write | Connect, configure, and remove social accounts (OAuth flows). |
publish:read | List scheduled posts and their status. |
publish:write | Schedule and publish content to connected accounts. |
events:read | Read the SDK analytics event stream. |
events:read+pii | Read events including unredacted personally-identifiable fields. Grant only when the integration needs PII. |
events:write | Forward server-side events / CAPI relay (counts against the customer's analytics + relay). |
metrics:read | Read organic and ads performance metrics. |
ads:read | Read ad accounts, campaigns, ad sets, ads, metrics, audit log, and the pending queue. |
ads:write | Umbrella ads-write — OAuth init + ads-content override. Prefer the fine-grained ads:write:* sub-scopes below for least-privilege keys. |
bootstrap:write | Run the end-to-end marketing-bootstrap onboarding workflow. |
media:read | List and read media-library assets. |
media:write | Upload, finalize, and delete media-library assets. |
influencers:read | List and read influencers. |
influencers:write | Create, clone, and patch influencers. |
leased:read | List and read leased-account requests. |
leased:write | Submit and release leased-account requests. |
engagement:read | Read auto-pilot engagement config. |
engagement:write | Patch auto-pilot engagement config. |
credits:read | Read the org credit balance, ledger, and per-format estimated costs. |
github:admin | Register and read GitHub App installations. |
jobs:read | Read async job status. |
jobs:cancel | Cancel running async jobs. |
webhooks:write | Register, rotate, replay, and delete webhook endpoints (read-side shares this scope today). |
org:admin | The sub-organization control plane: create / list / get / patch / suspend / resume / archive child organizations, mint child keys, allocate credits, and act on a child via the X-Layers-Organization header. Deny-by-default + non-delegable — see below. |
org:admin is deny-by-default and non-delegable. It governs minting customer orgs, draining wallets, and offboarding, so a key must carry it explicitly — no wildcard confers it (not even *), partner/internal-tier keys do not bypass it, and it can never be granted to a child key. It is minted only through the organization-management key flow, never selected alongside ordinary data scopes.
Parent and child orgs
When you run customers as child organizations, your top-level (parent) key carries org:admin. To operate inside a child — create a project for that customer, read its credits — send the X-Layers-Organization header with the parent key. A child that isn't a direct child of your org resolves to 404.
When you'd rather give a customer its own credential instead of driving it from the parent, mint a child-scoped key (below): a least-privilege key bound to one child org, with zero-downtime rotation.
Pending scope additions (this release)
The following scopes are planned for the partner API redesign and are documented on the surfaces that depend on them; they are not yet in the canonical PARTNER_API_SCOPES enum at the moment this page lands. Until they ship, the listed routes use the closest existing scope (typically projects:write for resource-create routes, ads:write for ads CAPI). Track the changelog for the exact ship date.
| Scope | Will gate | Status |
|---|---|---|
sdk:read / sdk:write | SDK app CRUD + verify-tracking | Pending — covered by projects:write / projects:read until shipped. |
Ads sub-scopes
The ads:* family is fine-grained. Mint only the sub-scopes the partner integration actually uses. Each corresponds to a specific endpoint class on the ads write surface.
| Scope | Gates |
|---|---|
ads:read | Reading campaigns, ad sets, ads, metrics, audit log, pending queue. |
ads:write:campaigns | Create / patch / delete campaigns. The simplified Create-campaigns endpoint requires this. |
ads:write:budgets | PATCH …/campaigns/:id/budget, …/adsets/:id/budget, daily/lifetime cap mutations. |
ads:write:creative | Push / replace / prune ad creatives. |
ads:write:lifecycle | Pause / resume / archive across the campaign / adset / ad hierarchy. |
ads:write:policy | Authority CRUD (PATCH …/campaigns/:id/authority, …/adgroups/:id/authority). Customer's own bucket-mode controls remain authoritative — this scope governs partner-issued authority changes inside the customer's policy. |
ads:write:optimizer_trigger | POST …/optimizer/run and the per-layer write-class flags. |
ads:write:pending | Approve / reject pending optimizer actions. |
ads:write:capi | Patch a project's CAPI config (Meta + TikTok). Wires through to project_layers.config.capi. |
The legacy single ads:write scope is mapped to the union of the sub-scopes above for backwards compatibility on existing keys; new keys should mint sub-scopes explicitly.
There is no ads:write:kill_switch scope. Kill-switch is customer-only; partner keys cannot trip it. Partners observe the kill-switch state passively via the audit log and the ads.write.denied webhook.
Wildcard scopes
The scope check accepts three wildcard forms — useful when minting an internal-tier key that should pass any future scope your endpoints adopt without re-issuing the key:
| Wildcard | Covers |
|---|---|
* | Every scope except the non-delegable org:admin, which always requires an exact grant. Reserved for internal/admin keys. |
<resource>:* | Every action on a resource. Example: ads:* covers ads:read, ads:write, every ads:write:<sub>, etc. |
<resource>:<action>:* | Every sub-scope under a resource:action pair. Example: ads:write:* covers all eight ads:write:* sub-scopes but does not cover ads:read. |
Wildcards expand at request time via hasScope() (api/src/lib/partner/scope.ts); they don't pre-mint sub-scopes onto the key. /v1/whoami returns the wildcard string verbatim — partners reading scope strings programmatically should evaluate wildcards rather than expecting expansion.
Empty scope lists are denied. A key with an empty/missing scopes array now grants nothing (deny-by-default) — it is not a "full access" passthrough. Keys that pre-date this change were migrated to the explicit * (god-mode) sentinel so they keep their prior access; every new key must be minted with at least one scope. Even the * god-mode wildcard does not confer the non-delegable org:admin — that scope requires an exact grant.
Creating API keys
Every key carries an explicit set of scopes chosen when the key is created. Because enforcement is deny-by-default, you must select at least one scope — a scope-less key can't call anything.
There are two ways to create a key:
- Your own organization's key — from the Layers dashboard under Org Settings → API Keys. The creation dialog has an expandable Permissions section: pick the scopes this key needs, then create. The full key secret is shown once on creation — copy it immediately.
- A per-customer child key — mint it programmatically against a child org with your parent (
org:admin) key viaPOST /v1/organizations/:orgId/api-keys. The request requires a non-emptyscopesarray (a subset of your own key's scopes); see child keys.
Choosing scopes
Grant the least privilege the integration needs — read scopes for read-only consumers, the matching :write scope only where the key actually mutates state. The scope table above lists every scope and what it gates. org:admin is never selectable here: it is minted only through the organization-management key flow and is never delegable to a child.
Scope is one-way
A key's scopes are fixed at creation and cannot be edited afterward. There is no "change permissions" operation — narrowing or widening a key's access means minting a new key with the desired scopes and revoking (or rotating out) the old one. This keeps a key's authority auditable and immutable for its whole lifetime; key lists display the granted scopes read-only.
Rate-limit tiers
Tier is a property of the key, not the endpoint. Buckets are keyed per (key_id, endpoint_class) so a runaway generation loop can't starve your reads. Endpoint classes: read-light, write-light, long-running.
| Tier | Typical provisioning |
|---|---|
standard | Default for every partner key. |
pilot | Higher throughput for early-integration partners - granted by Layers on request. |
partner | Enterprise tier for GA partners with SLAs. |
Every 429 response carries:
Retry-AfterX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-ResetX-RateLimit-Endpoint-Class,X-RateLimit-Tier- Body with
error.details.retryAfterMsanderror.details.endpointClass
See common patterns for a back-off snippet and rate limits for the full bucket policy.
Child keys for sub-organizations
If you manage customers as sub-organizations, you can mint API keys directly for each child org with your parent (org:admin) key. A child key is a per-customer credential: it's bound to exactly one child org and carries exactly the access you grant it.
Two rules govern what a child key may hold:
- Scope subsetting. A child key can only carry scopes you already hold. Request a scope your parent key doesn't have and the mint is rejected with
403 FORBIDDEN_SCOPE(details.offendingScopesnames the offenders). You can never grant a child more than you have. org:adminis never delegable. Even if your parent key holdsorg:admin(or a*wildcard), a child key can never receive it. The child must not run the control plane - minting its own children, moving projects, or draining wallets. Requestingorg:adminfor a child returns403 FORBIDDEN_SCOPE.
The full lifecycle is parent-driven and lives under the child org's path:
- Mint -
POST /v1/organizations/:orgId/api-keys. Secret returned once. - List - masked; never the secret.
- Rotate - zero-downtime, see below.
- Delete - immediate revoke.
These routes are anti-enumeration: a :orgId that isn't your direct child, or a :keyId that isn't that child's, both return 404 NOT_FOUND - a stranger's resource looks identical to a missing one. A malformed id returns 422 VALIDATION.
Rotation
You can rotate your own (parent / top-level) key without downtime:
Both keys are active in parallel during the cutover, so there's no flap.
Rotation grace window
Child keys rotate through a built-in API instead of a manual two-key dance. POST …/api-keys/:keyId/rotate mints a fresh key (new id, new secret, same scopes) and puts the old secret into a 24-hour grace window so there's no downtime while you roll out the new one:
graceUntil (when it dies) and supersededBy (the new key's id). Once graceUntil passes, the old secret returns 401 UNAUTHENTICATED.To cut the old secret off before the window closes, delete the old key - that's an immediate, grace-free revoke. A key rotates once: the rotation chain rolls forward (K → K2 → K3), and rotating an already-superseded key returns 409 CONFLICT - always rotate the current key.
The grace window never overrides a kill switch. A child key - new or old, in grace or not - is rejected with 503 KILL_SWITCH the moment its org is suspended or archived, or a per-key kill switch is flipped. Grace only postpones an old secret's normal expiry; it never resurrects access that ops has cut.
Revocation
Three levers, each stronger than the last:
- Revoke the key. Every subsequent request fails with
401 UNAUTHENTICATED. This is the normal path. - Kill switch on the key. Requests fail with
503 KILL_SWITCH- a different signal than revoked, so you can tell an incident from a rotation. Clearable without reissuing. - Org-wide kill switch. Every key on the org fails. Reserved for incident response.
Revocation and kill-switch flips take effect on the next request - propagation is effectively immediate.