---
title: Recover API Key
api: POST /auth/recover
description: "Start the API key recovery flow. Directs users to sign in to the dashboard via a magic link."
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://docs.boringmarketing.com/llms.txt). Append `.md` to any page URL for its markdown version.

Starts the API key recovery flow. This endpoint **never returns an API key in the response** — by design. The response directs the caller to sign in to the dashboard, where they can view or rotate their key after verifying ownership of the email via a magic link.

<Warning>
**This is not an account-existence check.** The response shape is identical whether the email matches a registered account or not — there is no `exists: true|false` field, no 404 branch, and the `message` / `dashboard_url` payload never varies. Agents that try to use `/auth/recover` as a lookup for "does this email have an account?" will be misled by the response body.
</Warning>

## Security: why the response is uniform

The handler is timing-padded at the Python level: regardless of whether the email resolves to a user, it executes the same sequence of database lookups (one real, one against a sentinel address) so that branch-count divergence doesn't stack on top of any underlying cache-hit vs cache-miss timing oracle in the user store. This **reduces branch asymmetry** but does not claim to be fully timing-proof — the underlying cache layer can still differ in how long a hit vs a miss takes, and that is not mitigated at this endpoint.

Combined with the rate limit below, the intent is to make email enumeration impractical, not mathematically impossible.

The endpoint is also rate-limited **3 requests per hour per IP**. Additional requests return `429` with a retry-after hint.

## Request

<ParamField body="email" type="string" required>
  The email address to recover. Lowercased and stripped server-side.
</ParamField>

## Response

```json
{
  "message": "Sign in to your dashboard to view or rotate your API key.",
  "dashboard_url": "https://dashboard.boringmarketing.com"
}
```

The response shape never varies. Even on an unknown email, you get the same `message` and `dashboard_url` — the dashboard's sign-in flow is what actually gates access to the key.

## Errors

| Status | Meaning |
|---|---|
| `422` | `email` is missing or not a string. |
| `429` | Rate limit hit (more than 3 requests per hour from this IP). |

## Example

<CodeGroup>
```bash cURL
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}' \
  https://api.boringmarketing.com/auth/recover
```

```python Python
resp = httpx.post(
    "https://api.boringmarketing.com/auth/recover",
    json={"email": "user@example.com"},
)
print(resp.json()["dashboard_url"])
```
</CodeGroup>

## Flow

<Note>
**Path split.** Only step 1 is programmatic — the rest of the flow is a browser-bound magic-link sign-in.

**If you're a human:** call the endpoint (or let an agent call it for you), then follow steps 2-5 in your browser.

**If you're an agent running unattended:** you can execute step 1 but cannot finish — there is no API path from a recovered session to the key itself. If you anticipate needing the key later, store it durably at issue time rather than recovering it.
</Note>

1. User (or agent on behalf of a user) calls `POST /auth/recover` with an email.
2. Server logs the attempt, runs the timing-padded lookup, and returns the dashboard URL.
3. User opens `dashboard_url`, signs in with a magic link sent to their email.
4. Once signed in, the dashboard calls [`GET /auth/me/api-key`](https://docs.boringmarketing.com/api-reference/auth/me-api-key) internally to hydrate the session with the user's key.
5. From the dashboard, the user can either view the key or call [`POST /auth/rotate-key`](https://docs.boringmarketing.com/api-reference/auth/rotate-key) to invalidate the old one and get a fresh one.
