# API Keys Guide

Use team API keys to call Last Chance from scripts, automation, or other machine clients without a browser session.

## Overview

API keys authenticate as your **team**, not as an individual user. Send the key on every request:

```http
Authorization: Bearer YOUR_API_KEY
```

Keys are created in **Account Settings → API Keys**. The full secret is shown **once** at creation — store it securely (password manager, secrets vault, or environment variable). You can revoke a key at any time; revoked keys stop working immediately and remain visible for audit.

**Who can manage keys:** Team admins only.

**Rate limits (API keys only):** Dashboard/session traffic is unaffected.

| Bucket                | Limit                            | Applies to                                                                                        |
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| Auth attempts         | **60 / minute** per IP           | Any `lc_live_…` Bearer (valid or invalid)                                                         |
| Global                | **30 requests / minute** per key | Dual-auth API routes (including tool-backed MCP calls); **not** the `/mcp` entry handshake itself |
| MCP entry             | **60 / minute** per key          | Remote MCP endpoint (`/mcp`) only — separate from the global bucket                               |
| Analytics summary     | **20 / minute** per key          | `/api/dashboard/stats`, `/api/analytics/unsubscribe-rates`                                        |
| Analytics attribution | **6 / minute** per key           | `/api/analytics/revenue-impact`                                                                   |

AI endpoints keep their existing tighter limits. Exceeding a limit returns `429`.

**MCP throughput:** Each MCP request counts **1×** against MCP entry and **1×** against the per-IP auth-attempt bucket at `/mcp`. Each `tools/call` then counts **1×** against global (plus analytics caps when those tools run) — in-process tool handlers do **not** re-charge auth-attempt. The practical ceiling is about **30 tool calls per minute per key** (the global bucket), even though MCP entry allows 60.

---

## Create a key

1. Open **Account Settings → [API Keys](/dashboard/settings/api-keys)**
2. Click **Create key**
3. Choose a name (for example `CI scripts` or `Billing sync`)
4. Pick scopes (defaults are read-only: `pages:read` and `team:read`)
5. Choose expiry (defaults to **90 days**; you can pick 30 / 90 / 365 days or never)
6. Copy the secret immediately — it will not be shown again

Limits:

- Up to **20 active** keys per team
- Create rate limit: **10 keys per hour** per team

---

## Connect with MCP (Claude / Cursor)

When **Connected MCP apps** in Account Settings shows that OAuth connections are available, OAuth
is the primary way to connect interactive Claude and Cursor clients to Last Chance’s hosted,
read-only MCP server. No API key is placed in the client configuration. If that section says new
OAuth connections are temporarily unavailable, use the API-key fallback below; existing API-key
connections continue working.

Prerequisites:

- A Last Chance account with an explicit membership in the team you want to connect
- A Claude or Cursor client that supports remote MCP servers and browser-based OAuth
- Permission to open a browser, sign in, choose a team, and approve read-only access

**Primary app host only.** Use `https://www.yourlastchance.app/mcp`. Branded unsubscribe hosts do not serve MCP or OAuth.

1. Open **Account Settings → [API Keys](/dashboard/settings/api-keys)**.
2. In **Connected MCP apps**, select **Add to Cursor** for one-click installation. The documentation
   renderer does not launch `cursor:` links directly, so the authenticated Account Settings button
   is the supported one-click entry point.
3. For another supported client, select **Copy config** and add the URL-only configuration to the
   client’s remote MCP settings:

```json
{
  "mcpServers": {
    "last-chance": {
      "url": "https://www.yourlastchance.app/mcp"
    }
  }
}
```

4. Start the connection in Claude or Cursor. Your browser opens Last Chance for sign-in, team selection, and consent.
5. Review the client name, verified host when available, team, and read-only data categories before approving.

### What the connector can access

The OAuth grant is tied to the selected team and provides read-only access:

- `get_team_context` — team tier, limits, and Klaviyo/Shopify connection flags
- `list_email_integrations` / `list_klaviyo_lists` — connected provider and list metadata
- `list_unsubscribe_pages` / `get_unsubscribe_page` / `list_page_versions` — page content and version discovery
- `list_page_templates` — safe template IDs, names, and descriptions
- `get_unsubscribe_page_urls` — operator preview URL for the published version
- `get_dashboard_stats` / `get_unsubscribe_rates` — aggregated performance metrics
- `get_revenue_impact` — aggregated attribution and revenue-impact reporting for a date range

The directory-launch OAuth connector exposes these 11 read-only tools and cannot create, edit,
publish, archive, or delete pages. It does not expose event-level email addresses.

Last Chance also has post-launch MCP mutation tools behind a server-side global gate and an exact
private-client allowlist. They are never available to DCR/CIMD directory clients and are not
advertised in public OAuth metadata. An allowlisted pre-registered client must explicitly request
`pages:write` and/or `pages:publish`; only explicit team ADMIN or SUPERADMIN members can approve
those scopes. API-key callers receive mutation tools only when the global gate is enabled and their
key has a write or publish scope. Publishing and permanent deletion also require an explicit
`confirm: true` tool argument.

### Example prompts

- “List our unsubscribe pages and tell me which one is currently primary.”
- “Compare our unsubscribe rate before and after Last Chance activation.”
- “Show revenue impact for July and explain the major funnel drop-offs.”
- “Which Klaviyo lists are connected, and what published URL should I send to a stakeholder?”

Your AI client decides when to send a tool call. Last Chance receives the selected tool name and its arguments and returns the requested result to that client. Last Chance does not receive the surrounding conversation unless the client includes some of it in a tool argument. See [Privacy](/privacy) and [Security guidance](#security-best-practices).

### Disconnect or revoke

Open **Account Settings → API Keys → Connected MCP apps**, choose the connection, and select **Revoke**. The approving member can revoke their own connection; team admins can revoke any connection for the team. You can also remove the server from Claude or Cursor. Revoking in Last Chance immediately invalidates that grant’s active credentials.

### Troubleshooting OAuth

| Problem                            | What to check                                                                                                        |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Browser sign-in does not open      | Confirm the client supports remote MCP OAuth and that the server URL is exactly `https://www.yourlastchance.app/mcp` |
| Redirect or registration error     | Update Claude/Cursor and retry; do not substitute a branded hostname or add a callback manually                      |
| Wrong team appears                 | Confirm your user is an explicit member of the intended team, then reconnect                                         |
| `401` after a previous connection  | The grant may be revoked or expired; remove the connection and complete OAuth again                                  |
| A tool reports insufficient access | OAuth is read-only; use the dashboard for changes                                                                    |

If the problem continues, contact [support@yourlastchance.app](mailto:support@yourlastchance.app) and include the request/correlation ID shown by your client. Never send access tokens or API keys.

### API-key fallback for scripts and CI

Team API keys remain supported for scripts, CI, automation, and clients that require a static `Authorization` header. Admins can create a key and copy the API-key configuration from this page. Prefer an environment variable or secrets vault; never commit the key.

```json
{
  "mcpServers": {
    "last-chance": {
      "url": "https://www.yourlastchance.app/mcp",
      "headers": {
        "Authorization": "Bearer ${env:LAST_CHANCE_API_KEY}"
      }
    }
  }
}
```

Set `LAST_CHANCE_API_KEY` in your environment to your `lc_live_…` secret (or paste the Bearer value directly — prefer the env var so the key is not committed).

### Agency portfolio keys

If your organization uses the **Partner** (agency) dashboard, partner admins can create **portfolio API keys** under **Partner → Settings**. These use the same `lc_live_…` format and the same hosted MCP URL, but they are stamped to the partner account (not a merchant team).

|                         | Team key (Account → API Keys) | Partner key (Partner → Settings)                                                   |
| ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
| Who creates             | Team admin                    | Partner admin                                                                      |
| Scopes                  | Pages, team, analytics        | `partner:read`, `partner:revenue:read`, `clients:read`                             |
| MCP tools               | Team pages / analytics tools  | Portfolio tools + `client_*` tools (`teamId` required; need `clients:read` to run) |
| Sees other clients’ MRR | No                            | Yes (your agency portfolio only)                                                   |

Merchant team keys **cannot** access partner portfolio data. Use a partner key when you want Cursor or Claude to reason about managed teams and Shopify-billed portfolio MRR. With `clients:read`, the same partner key can also read one attached client at a time (pages, team context, analytics) by passing that client’s team id — you do not need a separate team key per client.

### Available tools (read-only)

- `get_team_context` — team tier, limits, Klaviyo/Shopify flags
- `list_email_integrations` / `list_klaviyo_lists` — integrations and lists
- `list_unsubscribe_pages` / `get_unsubscribe_page` / `list_page_versions` — page discovery
- `get_unsubscribe_page_urls` — published-page preview URL only (machine callers cannot mint draft/share links)
- `get_dashboard_stats` / `get_unsubscribe_rates` — summary analytics (`analytics:summary:read`)
- `get_revenue_impact` — attribution analytics (`analytics:attribution:read`; requires `startDate` + `endDate`)

Partner portfolio tools (partner key only):

- `get_partner_context` — partner account + current-period portfolio rollup (`partner:read`)
- `list_partner_teams` — managed client teams + current MRR (`partner:read`)
- `get_partner_revenue` — historical monthly MRR series (`partner:revenue:read`)

Partner client tools (listed for partner keys; each requires `teamId` from `list_partner_teams` and `clients:read` at execution):

- `get_client_team_context` / `list_client_email_integrations` / `list_client_klaviyo_lists`
- `list_client_unsubscribe_pages` / `get_client_unsubscribe_page` / `get_client_unsubscribe_page_urls` / `list_client_page_versions`
- `get_client_dashboard_stats` / `get_client_unsubscribe_rates` / `get_client_revenue_impact`

For raw REST (same partner key), send `X-LC-Client-Team-Id: <clientTeamId>` on team GET routes. Optional `?teamId=` works as a fallback when the header is omitted; if both are sent they must match. Prefer the header. The team must be attached to your partner account. Writes are not granted by `clients:read`.

Example (partner key + attached client):

```bash
curl -sS "https://www.yourlastchance.app/api/team" \
  -H "Authorization: Bearer YOUR_PARTNER_API_KEY" \
  -H "X-LC-Client-Team-Id: CLIENT_TEAM_ID"
```

Revoke team keys in Account Settings → API Keys, or partner keys in Partner → Settings, to cut off access immediately.

---

## Authentication

Every machine request needs your API key in the `Authorization` header:

```bash
-H "Authorization: Bearer YOUR_API_KEY"
-H "Content-Type: application/json"
```

Replace `YOUR_API_KEY` with the full secret from Account Settings → API Keys. All examples below use `https://www.yourlastchance.app`.

Cookie/session auth still works for dashboard requests that do not present an `Authorization`
header. Any presented `Authorization` header is authoritative: unsupported schemes, malformed
values, and Bearer tokens that are not valid Last Chance credentials fail closed and never fall
back to cookies.

Machine page reads are always bounded: page lists and details use metadata-only projections, and
version lists use metadata-only pagination capped at 50 results. Query parameters cannot opt an API
key or OAuth caller into dashboard-sized page/version payloads.

---

## Scopes

| Scope                        | Allows                                                                                                                     |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `pages:read`                 | List and get unsubscribe pages and versions                                                                                |
| `pages:write`                | Create pages, save drafts, archive/primary, attach/detach email lists, toggle list webhooks, delete pages (with confirm)   |
| `pages:publish`              | Publish a draft version                                                                                                    |
| `pages:ai`                   | AI page chat and generate-from-website                                                                                     |
| `team:read`                  | Team context/limits, email integrations status, Klaviyo list discovery                                                     |
| `analytics:summary:read`     | Dashboard stats and unsubscribe rate trends (aggregated counts only — no email PII)                                        |
| `analytics:attribution:read` | Revenue impact / churn recovery attribution for a date range                                                               |
| `partner:read`               | Partner portfolio context, managed teams, and current-period MRR (partner keys only)                                       |
| `partner:revenue:read`       | Historical partner portfolio MRR series (partner keys only)                                                                |
| `clients:read`               | Read attached client teams via team GET APIs / MCP `client_*` tools when a client team id is specified (partner keys only) |

Grant the minimum scopes your client needs. A read-only team integration usually only needs `pages:read` and `team:read`. Add `analytics:summary:read` / `analytics:attribution:read` when the client should pull metrics. Partner scopes are available only on keys created under Partner → Settings. `clients:read` implies team read scopes for attached clients only (no page writes/publish/AI).

---

## Example requests

### Get team context

Requires `team:read`.

```bash
curl -sS "https://www.yourlastchance.app/api/team" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Returns team id/name, tier, Klaviyo/Shopify connection flags, subscription/access booleans, and page/AI/analytics limits. API-key responses omit billing provider IDs, shop domain, and other dashboard-only fields.

### List email integrations

Requires `team:read`.

```bash
curl -sS "https://www.yourlastchance.app/api/email-integrations" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### List Klaviyo lists

Requires `team:read`. Team keys use the team bound to the key. Partner keys with `clients:read` must target an attached client via `X-LC-Client-Team-Id` (same as other team GET routes).

```bash
curl -sS "https://www.yourlastchance.app/api/email-integrations/klaviyo/lists" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### List unsubscribe pages

Requires `pages:read`.

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Get one page

Requires `pages:read`. Machine callers get a metadata summary by default.

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### List versions and read page content

Requires `pages:read`. Machine callers get metadata-only versions, capped at 50 per request. Each
version lists its `componentIds` (bounded to 20, with `componentIdsTruncated` when there are more).

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID/versions?page=1&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Read one component at a time using a `versionNumber` from that list and one of its `componentIds`:

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID?mcpProjection=component&versionNumber=2&componentId=initial" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

A component larger than 400 KiB returns `413` with `code: "CONTENT_COMPONENT_TOO_LARGE"`; open that
version in the dashboard editor instead.

### List page templates

Requires `pages:read`. Returns only bounded template IDs, names, and descriptions. Use an ID from
this catalog with the create endpoint; template HTML is resolved server-side.

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages/templates" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Get the published preview URL

Requires `pages:read`. API-key and OAuth callers receive only `previewUrl` for the currently
published version (using your branded public host when configured). The URL always targets the
requested unsubscribe page, including when it is not the team's primary page.

Machine credentials cannot mint draft or public share links. Create those links from the
authenticated dashboard when stakeholder review is needed. Do not send `previewUrl` as a public
review link because it is an operator preview flow.

```bash
curl -sS "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID/urls" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Create a page from a template

Requires `pages:write`. Unpaid teams are limited to 3 pages total; paid teams are unlimited.

```bash
curl -sS -X POST "https://www.yourlastchance.app/api/unsubscribe-pages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stay offer page",
    "templateId": "TEMPLATE_ID"
  }'
```

### Save a draft version

Requires `pages:write`.

```bash
curl -sS -X POST "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID/versions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Draft from API",
    "expectedLatestVersionNumber": 1,
    "globalStyles": {},
    "pages": [
      {
        "id": "initial",
        "enabled": true,
        "html": "<div data-action=\"stay\">Stay subscribed</div>"
      }
    ]
  }'
```

Required fields for API-key clients: `name`, `expectedLatestVersionNumber`, `globalStyles`, and
`pages` (an **array** of page objects). The server assigns the next version number. Fetch the current version first and send
its number as `expectedLatestVersionNumber`; a concurrent edit returns `409` with
`code: "VERSION_CONFLICT"` and the actual latest number. Use the same JSON shape the dashboard
editor saves (HTML/CSS, theme, offers). Omitted optional draft fields preserve their latest values;
send `null` to clear nullable description, CSS, theme, or discount fields. Unchanged content may
return the existing version instead of creating a new one.

### Publish a version

Requires `pages:publish`. Publishing requires an active subscription.

```bash
curl -sS -X POST "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID/versions/VERSION_ID/publish" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm": true, "expectedLatestVersionNumber": 7}'
```

`expectedLatestVersionNumber` is optional and guards against publishing from a stale view: send the
version number you believe is the newest draft, and the publish is refused with `409` and
`code: "VERSION_CONFLICT"` (plus the actual latest number) if someone saved past you. Omit it only
when you mean to roll the live page back to an older version.

The page must have its own active Klaviyo integration, and the list ID must be accessible through
that integration. Last Chance verifies the ID with Klaviyo and stores Klaviyo's canonical list name.

### Attach a Klaviyo list to a page

Requires `pages:write`.

```bash
curl -sS -X POST "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID/email-lists" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "listId": "KLAVIYO_LIST_ID",
    "listName": "Newsletter",
    "provider": "klaviyo"
  }'
```

### Delete a page (API key only)

Requires `pages:write` **and** `confirm=true` (query or JSON body). Session deletes from the dashboard do not need this flag.

```bash
curl -sS -X DELETE "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID?confirm=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Or:

```bash
curl -sS -X DELETE "https://www.yourlastchance.app/api/unsubscribe-pages/PAGE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm": true}'
```

### Dashboard stats (summary)

Requires `analytics:summary:read`. Returns aggregated stay/unsubscribe counts, visitor retention, and attributed order revenue for the team (no per-email data).

```bash
curl -sS "https://www.yourlastchance.app/api/dashboard/stats" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Unsubscribe rates (summary)

Requires `analytics:summary:read`. Compares Klaviyo unsubscribe rates before/after Last Chance activation. Optional `startDate` / `endDate` (ISO). Requires an active Klaviyo integration.

```bash
curl -sS "https://www.yourlastchance.app/api/analytics/unsubscribe-rates" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Revenue impact (attribution)

Requires `analytics:attribution:read`. Full-funnel churn recovery and revenue metrics. **`startDate` and `endDate` are required** (ISO date strings). Optional: `enforceActivationDate`, `variantId`, `attributionWindowMode`. Requires an active Klaviyo integration.

```bash
curl -sS "https://www.yourlastchance.app/api/analytics/revenue-impact?startDate=2026-01-01&endDate=2026-03-31" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Event-level analytics that include email addresses are **not** available via API keys.

---

## Common errors

| Status | Code / message                     | Meaning                                                                        |
| ------ | ---------------------------------- | ------------------------------------------------------------------------------ |
| `401`  | Unauthorized                       | Missing/invalid key, revoked key, or expired key                               |
| `403`  | `insufficient_scope`               | Key lacks a required scope (partner client reads need `clients:read`)          |
| `403`  | `partner_key_requires_client_team` | Partner key on a team endpoint without `X-LC-Client-Team-Id` (or `?teamId=`)   |
| `403`  | `partner_client_not_attached`      | Partner key targeted a team that is not attached to the partner account        |
| `403`  | `partner_client_read_only`         | Partner key attempted a write/mutation on a client team route                  |
| `400`  | `client_team_id_mismatch`          | `X-LC-Client-Team-Id` and query `teamId` disagree                              |
| `403`  | `partner_key_not_allowed`          | Invalid partner/team key stamp (should not happen for keys created in the app) |
| `403`  | `team_key_not_allowed`             | Merchant team key used on a partner portfolio endpoint                         |
| `403`  | Active subscription required       | Publish (and some paid features) need a paid team                              |
| `400`  | `CONFIRMATION_REQUIRED`            | API-key page delete missing `confirm=true`                                     |
| `403`  | `API_KEY_LIMIT_REACHED`            | Team or partner already has 20 active keys                                     |
| `429`  | Rate limited                       | Too many API-key requests, AI calls, or key creates                            |
| `503`  | `rate_limit_unavailable`           | Rate limiter temporarily unavailable (retry shortly; key create blocked)       |

Responses never include Klaviyo OAuth secrets (`accessToken`, `refreshToken`, etc.).

---

## Security best practices

- Prefer short-lived keys (90 days) and rotate before expiry
- Use the smallest scope set that works
- Store secrets in env vars or a vault — never commit them
- Revoke keys you no longer need; check **Last used** on the API Keys page
- Do not share one key across unrelated systems if you can avoid it

---

## Related

- [Settings](/docs/settings) — Other account configuration
- [Getting Started](/docs/getting-started) — Create and publish your first page in the UI
- Manage keys in-app: [Account Settings → API Keys](/dashboard/settings/api-keys)
