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

# Keys

# Keys (API Key Lifecycle)

> **Endpoint reference: ⚠️ JWT-auth required.**
> The `/keys` endpoints are how a holder generates, lists, and revokes the API key pairs they hand to store builders. Store builders **don't call these directly** — they consume the resulting `(apiKey, secretKey)` pair. This page documents the lifecycle so you understand how key rotation works and what to expect when a holder revokes a pair.

***

## Endpoints

| Method  | Path           | Auth | Description                                           |
| ------- | -------------- | ---- | ----------------------------------------------------- |
| `POST`  | `/keys`        | JWT  | Generate a new `(apiKey, secretKey)` pair             |
| `GET`   | `/keys`        | JWT  | List the holder's active key pairs (public part only) |
| `PATCH` | `/keys/{id}`   | JWT  | Update a key (e.g. label)                             |
| `PATCH` | `/keys/delete` | JWT  | Revoke (soft-delete) one or more keys                 |

> ⚠️ **All key endpoints require a JWT, not an API-key signature.** A store builder cannot generate or revoke keys for a holder — only the holder can, via the DAPP UI (which holds the JWT).

***

## Key lifecycle at a glance

```
Holder logs into DAPP UI (JWT obtained via /auth/dapp/signIn)
    │
    ▼
POST /keys  →  receives { apiKey, secretKey }
    │       (this is the ONLY time the secretKey is returned;
    │        if the holder loses it, they must generate a new pair)
    ▼
Hands { apiKey, secretKey } to store developer (out-of-band — encrypted email,
                                                  password manager share, etc.)
    │
    ▼
Store uses the pair on every /dapp/* request
    │
    ▼
PATCH /keys/delete  →  revokes the pair
    │       (subsequent requests using the pair return 401)
    ▼
Holder generates a new pair if needed and re-shares with store
```

***

## `POST /keys` — generate a new pair

Creates a new ECDSA secp256k1 keypair on the server, stores the public key, and returns both halves to the caller.

### Request body

Per `ApiKeyRequestDto`:

```json theme={null}
{
  "label": "Production WooCommerce Store"
}
```

| Field   | Type   | Required | Notes                                                                                                |
| ------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- |
| `label` | string | ✅        | Human-readable name shown in the DAPP UI keys list. Be descriptive: include environment and purpose. |

### Response

`201 Created`. The response is the **only** time the `secretKey` is returned in plaintext.

🔒 Exact response shape pending live capture. Conventional shape:

```json theme={null}
{
  "success": true,
  "statusCode": 201,
  "message": "Success",
  "data": {
    "id": "<key-uuid>",
    "label": "Production WooCommerce Store",
    "apiKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0K...",
    "secretKey": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCg...",
    "createdAt": "2026-05-10T12:00:00.000Z"
  }
}
```

| Field       | Type              | Notes                                                         |
| ----------- | ----------------- | ------------------------------------------------------------- |
| `id`        | string (UUID)     | Internal identifier for management ops                        |
| `label`     | string            | As submitted                                                  |
| `apiKey`    | string            | Base64-encoded PEM SPKI public key — safe to store            |
| `secretKey` | string            | Base64-encoded PEM PKCS8 private key — **handle as a secret** |
| `createdAt` | string (ISO 8601) |                                                               |

### What the holder must do with the pair

1. **Copy both values.** The DAPP UI will warn that `secretKey` is shown once.
2. **Hand off via a secure channel.** Encrypted email, 1Password share, or in-person. Never plain SMS or unsecured Slack.
3. **The store stores the pair in their secrets manager** — AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or at minimum environment variables in a secure deploy pipeline.

### Limit

A holder can have up to **100 active key pairs**. Hitting the limit returns an error; the holder must `PATCH /keys/delete` an unused pair before generating a new one.

***

## `GET /keys` — list active pairs

Returns the holder's key pairs *without* secret keys (those are unrecoverable after creation).

### Canonical payload (if reachable via API key — currently JWT-only)

`{}` — but again, this endpoint requires JWT in practice.

### Response

🔒 Pending live capture. Expected shape:

```json theme={null}
{
  "data": {
    "keys": [
      {
        "id": "<uuid>",
        "label": "Production WooCommerce Store",
        "apiKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0K...",
        "isActive": true,
        "createdAt": "2026-05-10T12:00:00.000Z",
        "lastUsedAt": "2026-05-10T15:32:11.428Z"
      }
    ]
  }
}
```

The `lastUsedAt` field (if present) gives the holder visibility into stale keys — useful for cleanup before hitting the 100-key limit.

***

## `PATCH /keys/{id}` — update a key

Updates a key's metadata. Typically used to rename:

```json theme={null}
{
  "label": "Production WooCommerce Store (v2)"
}
```

Cannot rotate the keypair via this endpoint — to change the secret, the holder must generate a new pair and revoke the old one.

🔒 Full PATCH semantics not yet captured live.

***

## `PATCH /keys/delete` — revoke (soft-delete)

Marks one or more keys as deleted. **Soft-delete** — the keys are not physically removed from the database, but subsequent authenticated requests using them return `401`.

### Request body

Per `SoftDelete` DTO:

```json theme={null}
{
  "ids": [
    "<key-uuid-1>",
    "<key-uuid-2>"
  ]
}
```

| Field | Type             | Required | Notes                           |
| ----- | ---------------- | -------- | ------------------------------- |
| `ids` | array of strings | ✅        | One or more key UUIDs to revoke |

### Response

`200 OK` with a count of revoked keys.

> 🪲 **Note the unusual REST shape.** This is a `PATCH` to `/keys/delete` rather than a `DELETE` to `/keys/{id}` or `DELETE /keys` with a body. It's a backend convention; just match it.

### What happens after revocation

* All in-flight requests using the revoked pair return `401 "User is not authorized"` (the DAPP guard rejects).
* The holder can re-issue a new pair at any time.
* **There's no notification to the store** — your store will start seeing 401s unexpectedly. Build for this:

```ts theme={null}
async function withKeyRotationHandling(fn: () => Promise<unknown>) {
  try {
    return await fn();
  } catch (e: any) {
    if (e.statusCode === 401 && e.message === 'User is not authorized') {
      // Likely revoked. Surface to ops, don't silently retry.
      await alertOps('Dr Green API key may have been revoked');
    }
    throw e;
  }
}
```

***

## What store builders should know

You don't call any of these endpoints. But you **should**:

### Have a key-rotation runbook

Document the steps for when:

* A key is suspected compromised
* An employee with access leaves
* The annual rotation date arrives

The runbook should be:

1. Holder logs into DAPP UI
2. Generates new pair → labels it clearly (`<store-name> rotation 2026-Q2`)
3. Updates secrets manager with new pair
4. Deploys / reloads store config
5. Verifies a few read calls succeed with new pair
6. Revokes the old pair via DAPP UI
7. Verifies old pair now returns 401 (sanity check)

### Plan for sudden 401s

A holder might revoke without warning. Your store should:

* Detect the rotation-flavored 401 ("User is not authorized" with otherwise-correct signing)
* Page your on-call
* Surface a clear "store temporarily disconnected from Dr Green" status to customers in flight

### Never request the holder's wallet credentials

The wallet is the root of trust. The holder uses it to sign the SIWE message, get a JWT, and generate API keys — and that's where your involvement starts. **Never** ask for the wallet seed phrase, private key, or anything beyond the API key pair. If a Dr Green integration ever asks for those, it's compromised.

***

## See also

* [Authentication](../02-authentication.md) — how the keys are used in every request
* [Auth (Holder Sign-In)](./auth.md) — how the holder gets the JWT that lets them generate keys
* [04-errors.md § 401 Unauthorized](../04-errors.md#common-failure-modes-and-how-to-diagnose-them) — diagnosing key-related 401s
