# Authentication > LaunchList API keys are bearer tokens prefixed llk_live_, or llk_test_ for a test key. They are workspace-owned, scoped, and can be pinned to a single project. Source: https://getlaunchlist.com/help/docs/api/authentication --- Every request to the LaunchList API carries an API key as a bearer token: ```bash curl https://api.getlaunchlist.com/v1/workspace \ -H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk" ``` Keys are created in **Embed & API → Developers → API keys** in the dashboard: press **New API key**, name it, choose its **Mode**, its **Access** and which **Projects** it covers, and press **Create key**. ![Embed & API, Developers, API keys, with the New API key button at the top right.](https://app.getlaunchlist.com/images/docs/api/authentication/01-api-keys-list.webp "Keys are made under Embed & API, Developers, API keys.") ![The New API key panel with a name field, a choice of Live or Test mode, and access and project options.](https://app.getlaunchlist.com/images/docs/api/authentication/02-new-key-dialog.webp "Name the key, then choose its mode and access.") ## How keys work A key is **owned by the workspace**, not by the person who made it, so removing a team member never breaks a running integration. The plaintext is shown **exactly once**, at creation. Only a SHA-256 hash is stored, so a lost key cannot be recovered: revoke it and create another. A key is the `llk_live_` prefix followed by 40 random characters, and the prefix is deliberate, because it makes a leaked key recognizable to automated secret scanners on GitHub and elsewhere. ## Live and test keys A **live** key (`llk_live_…`) reads and writes your real signups. A **test** key (`llk_test_…`) reads and writes test data only: every contact it creates is a test contact, it never sees or changes a real contact, and anything that would reach your real project, such as a broadcast or an import, answers `403 test_mode_unsupported`. You pick the mode when you create the key, and it never changes. A test key is made without the scopes it could never use: the `broadcasts:*`, `imports:write`, `segments:write` and `pages:*` scopes. Every response says which kind of key answered in its `LaunchList-Mode` header. What a test key can and cannot do is in [testing without touching your audience](/help/docs/api/openapi#testing-without-touching-your-audience). Test keys work with the REST API only, not with the MCP server. > [!WARNING] Never put a live key in client-side code > A key in browser JavaScript is a public key. To capture signups from a web page, use the > [embed](/help/docs/integrations/carrd) instead, which is designed to be public. ## Scopes When you create a key you pick one of two access levels: | Access level | Scopes it carries | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Read-only** | Every `:read` scope: `contacts:read`, `projects:read`, `pages:read`, `broadcasts:read`, `segments:read`, `automations:read` | | **Full access** | Every scope, subject to what you personally may grant (see below) | Choose **Read-only** for anything that only reads (analytics, exports, a dashboard) and **Full access** for a key that writes: creating or updating contacts, drafting pages, importing a list, tagging, saving segments, issuing invitations, or sending broadcasts. A request that calls an endpoint the key's scopes do not cover returns `403` with `insufficient_scope`. Each scope grants: | Scope | Grants | | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | `contacts:read` | List, retrieve, count contacts, read their submissions, referrals, position and email history, read and redeem invitations | | `contacts:write` | Create, update and delete contacts, resend verification, resubscribe and unsubscribe | | `projects:read` | List and retrieve projects and forms, and read `/v1/workspace` | | `pages:read` | List and retrieve a project's hosted pages | | `pages:write` | Over MCP: set up a project, draft a hosted page from a description, edit its draft, and publish it in two confirmed steps. It never edits a live page in place | | `invites:write` | Mint invitations, revoke them, and confirm access without a code | | `broadcasts:read` | List broadcasts, their results and their recipients | | `broadcasts:write` | Create and edit broadcast drafts, and send a test to a teammate. Never sends to the audience | | `broadcasts:send` | Send, schedule, pause, resume and cancel a broadcast to the audience | | `segments:read` | List segments, and count who an unsaved filter matches | | `segments:write` | Save a segment | | `imports:write` | Bulk-import contacts without the signup pipeline, exactly like the dashboard's CSV import | | `automations:read` | List automations and see where a contact stands in each | Redeeming a code sits under `contacts:read` on purpose. It is what your signup flow calls on every new account, so a **Read-only** key can redeem invitations, while minting and revoking them needs `invites:write`, because they hand out access to your own product. > [!NOTE] A key never carries a scope you could not grant yourself > A key is a standalone credential that skips the dashboard's own permission checks on every > request, so scopes with real consequences are gated on your team role when the key is minted: > `contacts:write` and `segments:write` need **Edit signups, tags, and segments**, `imports:write` > needs **Import a list**, `broadcasts:read` and `automations:read` need **View campaigns and > automations**, `broadcasts:write` needs **Build campaigns and automations**, and `broadcasts:send` > needs **Send to the audience**. Minting a **Full > access** key as a member who cannot send campaigns produces a key without `broadcasts:send`; the > **Read-only** preset is unaffected, since no `:read` scope needs anything beyond the permission > to create a key at all. The exact scope each endpoint needs is machine-readable in the [OpenAPI document](/help/docs/api/openapi), on every operation as `x-required-scope`. ## Pinning a key to one project A key can be limited to a single project when you create it. A pinned key cannot read or write any other project in the workspace, and attempts return `404` rather than `403`. That is deliberate: a `403` would confirm the other project exists. Across a tenant boundary, we never confirm that an ID is real. ## Keeping a staging integration off your real audience Give it a **test key**. Everything it writes is test data, so a misfiring integration cannot write into the audience you actually launch to. For what a test key cannot do, such as sending a broadcast, create a **separate project** and pin a live key to it: a pinned key reaches nothing outside that project. ## Checking a key `GET /v1/workspace` introspects the key: the workspace it belongs to, the plan, the key's own scopes and mode, the per-minute rate limit, and the current contact count. ```bash curl https://api.getlaunchlist.com/v1/workspace \ -H "Authorization: Bearer llk_live_8xKq3Rp9Wm2Nz7Vt6yL1cG5dH0jA4sE8uNqB2fXk" ``` > [!NOTE] Both access levels can call this > `GET /v1/workspace` needs `projects:read`, which both **Read-only** and **Full access** carry, so > it works as a health check for any key you can create. `401` is the only answer that means the key > is dead: revoked, expired, or wrong. A `200` means it is alive. ## Rate limits Limits are per workspace, per minute, and set by plan: | Plan | Requests per minute | | ------- | ------------------- | | Free | 60 | | Collect | 300 | | Launch | 300 | | Grow | 1000 | Every authenticated response carries `RateLimit-Limit` and `RateLimit-Remaining`. A rejected key is the exception: a `401` is answered before the rate limiter runs, so it carries neither header. Over the limit you get a `429` carrying `Retry-After`, which is the number of seconds to wait. Full detail, including the separate limits on unauthenticated and invitation-redemption traffic, is in [rate limits](/help/docs/api/rate-limits). ## Common questions ### How do I authenticate with the LaunchList API? Send your API key as a bearer token in the Authorization header, written as "Authorization: Bearer" followed by the key. Keys are created in Embed & API → Developers → API keys. ### What happens if I lose a LaunchList API key? You cannot recover it. The plaintext key is shown exactly once at creation and only a hash is stored. Revoke the lost key and create a new one. ### Does an API key stop working when the person who created it leaves? No. Keys are owned by the workspace, not by the user who created them, so removing a team member never breaks a running integration.