> ## Documentation Index
> Fetch the complete documentation index at: https://lmn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication & Security

> API key + IP allowlist + idempotency. Defense in depth.

## API Key

* **Header**: `x-api-key`
* **Format**: `lmn_prd_<28 url-safe base64 chars>` (production) or `lmn_stg_<28 url-safe base64 chars>` (sandbox).
* **Issuing**: mint your own in the [partner console](https://console.lmnauto.com/console/signup) → **Keys**. Sandbox keys are self-serve and instant; production keys unlock after approval.
* **Transport**: the key is displayed **once**, at creation. LMN stores only a hash and cannot show it to you again — capture it then, into a secret manager. Never commit to source; never transmit in URLs.
* **Rotation**: self-service from the console (**Keys → Rotate**). **The old key is revoked immediately** — it stops working the moment the new key is issued, with no overlap window. See [Rotating without downtime](#rotating-without-downtime) for the safe procedure.
* **Errors**:
  * `401 missing_api_key` — `x-api-key` header absent.
  * `401 invalid_api_key` — key not recognized, revoked, or wrong environment (e.g., `lmn_stg_*` against `api.lmnauto.com`).

## Rotating without downtime

**Keys → Rotate** is create-then-revoke, and both halves complete server-side before the response reaches you. By the time you are looking at the new key, the old one is already revoked — there is no window in which both work. If your deployment takes ten minutes, that is ten minutes of `401 invalid_api_key`.

To rotate with no gap, use two keys instead of the Rotate button:

<Steps>
  <Step title="Create a second key">
    **Keys → Create**. Both keys are now valid.
  </Step>

  <Step title="Deploy the new key">
    Roll it out to every caller and confirm traffic is flowing on it.
  </Step>

  <Step title="Revoke the old key">
    **Keys → Revoke** on the original. Nothing is depending on it any more.
  </Step>
</Steps>

Trial accounts may hold **2 active sandbox keys**, which is exactly enough to run this procedure from a single key. Production accounts are uncapped. If a trial account is already at the cap, **Create** returns `403` — but **Rotate** deliberately bypasses the cap so you are never locked out of replacing a key, at the cost of the gap described above.

<Note>
  **Revocation propagates immediately in the normal case**, and within \~60 seconds in the worst case (key state is cached for 60s, with cross-pod invalidation on revoke). Treat a revoked key as dead the instant you revoke it — never plan a cutover around the propagation lag.
</Note>

Use **Rotate** when the key is compromised and killing it immediately is the point. Use the two-key procedure for routine, scheduled rotation.

## X-User-Id (optional, attribution)

Send an `X-User-Id` header on **any** request to attribute it to the individual end-user/dealer within your org. Useful for per-dealer analytics and curation.

```
GET /v1/vehicles HTTP/1.1
x-api-key: lmn_prd_...
X-User-Id: your-internal-user-id
```

* **Optional** — omitting it is never an error.
* **Not authenticated** — LMN does not validate, resolve, or reject the value. Authentication is solely on `x-api-key`. This is opaque attribution metadata only.
* **Format** — any stable opaque string; your namespace to define. Send the **same value** for the same user so attribution groups correctly (grouping is by exact, case-sensitive value). `X_User_Id` is accepted as an alternate header name.
* **Privacy** — LMN's request logs store a one-way SHA-256 hash of the value, not the raw string.
* **Where used** — LMN's logging/audit pipeline only; not stored on order resources.

## IP Allowlist

<Note>
  **Sandbox trial keys have no allowlist.** Keys minted by a self-serve account accept requests from any source IP, so you can build from a laptop, a CI runner, or a serverless platform with rotating egress. Nothing below applies until you move to production.
</Note>

Production keys are accepted only from pre-registered partner egress IPs.

* Up to **16 addresses or CIDR ranges** per environment — IPv4 and IPv6 both supported (e.g., `203.0.113.0/24`, `2001:db8::/32`).
* Sandbox and production have independent allowlists.
* Requests from unlisted IPs return `403 ip_not_allowed`.
* Matching is per-family: if your client egresses over both IPv4 and IPv6 (dual-stack), register ranges for **both** families — otherwise requests intermittently fail when the unlisted family is used.

Manage entries yourself in the partner console: **Keys → the key → Allowlist**. Changes take effect within a minute (keys are cached for up to 60 seconds). For emergency revocation outside the console, contact `security@lmnauto.com`.

<Warning>
  **Serverless backends and production keys.** Platforms like Vercel, Supabase Edge Functions, Lovable Cloud, and Cloudflare Workers egress from large, rotating IP pools that cannot be meaningfully allowlisted. Certify your integration on a sandbox key, and raise the deployment target with us during production approval so we can plan a workable egress setup — a static-IP NAT or a dedicated proxy — rather than discovering it at cutover.
</Warning>

## TLS Requirements

* Minimum **TLS 1.2** (TLS 1.3 preferred).
* Forward-secret cipher suites required (ECDHE-\*).
* Public CA chain — no certificate pinning required.

## Idempotency-Key (required on order creation)

`POST /v1/orders` must include an `Idempotency-Key` header with a UUID v4 value.

```
Idempotency-Key: 8e8b2a70-7a4f-4f7c-9e3a-8ed4f8c3a9d1
```

* LMN enforces a **permanent unique constraint** on `(api_key, idempotency_key)`. Replays return the cached response (same status, same body). Safe to retry on timeouts.
* Missing on `POST /v1/orders` returns `400 missing_idempotency_key`.
* Reusing an Idempotency-Key with a **different** `vehicle_id` — or with a different `secondary_inspection_required` for the same vehicle — returns `422 idempotency_key_reused`. Generate a fresh key per logical request, never reuse across different orders.

### Safe vs unsafe key reuse

| Scenario                             | Result                                                                                                                                                              | Example                                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Same key + same body (network retry) | ✅ Safe — returns the original response, no duplicate created                                                                                                        | Same `vehicle_id`, same `max_bid_amount_usd`                                                                       |
| Same key + different body            | ❌ `422 idempotency_key_reused` with `details.original_vehicle_id` (vehicle mismatch) or `details.original_secondary_inspection_required` (inspection-flag mismatch) | Different `vehicle_id` — or same vehicle with a different `secondary_inspection_required` — than the original POST |
| Different key + same body            | ⚠️ Treated as a new request — `409 duplicate_order` if active order already exists for that vehicle                                                                 | Generated a fresh key for what should have been a retry                                                            |

**Rule of thumb:** generate the Idempotency-Key once at the moment of order intent, then reuse it across **all retries of the same logical request**. Never reuse across different logical requests.

## Rate Limits

Fair-use targets per API key (not currently enforced server-side, except the eagle-eye test-webhook limiter — 5 fires per minute per watch, which does return `429`):

| Bucket     | Endpoints                                     | Target          |
| ---------- | --------------------------------------------- | --------------- |
| Read-heavy | `GET /v1/vehicles`, `GET /v1/vehicles/facets` | **600 req/min** |
| Read-light | `GET /v1/orders/{id}`, `GET /v1/orders`       | **300 req/min** |
| Write      | All `POST`, `DELETE`                          | **60 req/min**  |

Build your client as if these were enforced: stay under the targets and apply exponential backoff to transient failures. Server-side enforcement at these targets may be introduced later without a breaking-change notice — clients that already pace themselves won't be affected. Sustained traffic far above the targets may be raised with your integration contact.

<Warning>
  **Do not map `429 rate_limited` to a single behavior.** On `/v1` that code covers two conditions with opposite remedies, and neither sends `Retry-After`:

  * **[Test webhook throttle](/errors#test-webhook-throttle)** — 5 fires/min per watch on `POST /v1/eagle-eye/watches/{watch_id}/test-webhook`, the one throttle that *is* enforced. Transient; clears within 60s.
  * **[Trial budget exhausted](/errors#trial-budget-exhausted)** — a sandbox trial key's lifetime call budget is spent. Permanent; retrying never succeeds.

  Branch on `error.details.limit`: present means budget, absent means throttle. See [Telling the two 429s apart](/errors#telling-the-two-429s-apart). If general burst throttling is introduced later it will be documented before it ships.
</Warning>

## Webhook signing

All webhooks LMN sends to your endpoint are signed with HMAC-SHA256 (Stripe-compatible scheme). See [Webhooks → Signing](/webhooks/signing-and-retries) for verification details.
