# Authentication (/docs/api/getting-started/authentication)



The API integration 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 [#key-shape]

```text
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 [#where-to-put-the-header]

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

```http
Authorization: Bearer lp_...
```

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

<Tabs items="['curl', 'fetch', 'axios', 'Python requests']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/whoami \
      -H "Authorization: Bearer $LAYERS_API_KEY"
    ```
  </Tab>

  <Tab value="fetch">
    ```ts
    await fetch("https://api.layers.com/v1/whoami", {
      headers: { "Authorization": `Bearer ${process.env.LAYERS_API_KEY}` },
    });
    ```
  </Tab>

  <Tab value="axios">
    ```ts
    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");
    ```
  </Tab>

  <Tab value="Python requests">
    ```python
    import os, requests

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

    session.get("https://api.layers.com/v1/whoami")
    ```
  </Tab>
</Tabs>

## Scopes [#scopes]

<Callout>
  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).
</Callout>

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](/docs/api/concepts/organizations); allocate credits; mint child API keys; act on a child via the `X-Layers-Organization` header. **Non-delegable** (see below).                |

<Callout type="warn">
  `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).
</Callout>

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 [#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 [#acting-on-behalf-of-a-child-org]

If you run customers as [sub-organizations](/docs/api/concepts/organizations), your parent key can operate *inside&#x2A; a child org by sending the &#x2A;*`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:

```http
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`](#scopes) 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.

<Tabs items="['curl', 'fetch', 'Python requests']">
  <Tab value="curl">
    ```bash
    # 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"
    ```
  </Tab>

  <Tab value="fetch">
    ```ts
    await fetch("https://api.layers.com/v1/credits", {
      headers: {
        "Authorization": `Bearer ${process.env.PARENT_KEY}`,
        "X-Layers-Organization": "org_cust_a",
      },
    });
    ```
  </Tab>

  <Tab value="Python requests">
    ```python
    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",
        },
    )
    ```
  </Tab>
</Tabs>

### Or mint a dedicated child key [#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](/docs/api/concepts/api-keys#child-keys-for-sub-organizations) 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`](/docs/api/reference/organizations/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 [#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 [#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 [#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-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](/docs/api/operational/rate-limits) for the full bucket table and 429 envelope.

## Best practices [#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](/docs/api/getting-started/common-patterns#idempotency).
