> ## 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.

# 04 errors

# Errors

> **TL;DR.** Every Dr Green API response — success or failure — uses the same envelope: `{ success, statusCode, message, data }`. On error, `success` is `false`, `statusCode` is the HTTP status, `message` describes what went wrong, and `data` is usually `null` or an array of validation issues.

***

## Response envelope

A NestJS global response interceptor wraps every response in this shape:

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": { /* endpoint-specific payload */ }
}
```

**Every** authenticated endpoint returns this shape. So does the public `/healthStatus` check. Build your client around this envelope from day one — don't unwrap manually per-endpoint.

### Successful responses

```json theme={null}
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": { "id": "abc123", "..." }
}
```

`statusCode` is `200` for retrievals/updates and `201` for resource creation, mirroring the HTTP status of the response itself.

### Error responses

```json theme={null}
{
  "success": false,
  "statusCode": 400,
  "message": "Validation failed",
  "data": [
    "email must be a valid email",
    "phoneNumber should not be empty"
  ]
}
```

When the failure is a validation error from `class-validator`, `data` contains a string array of human-readable validation messages — one per field violation.

For other errors (auth, not-found, conflict), `data` is typically `null` and `message` is the only useful field. Always surface `message` to your developer-facing logs.

***

## Status codes you should handle

| Code                          | Meaning                                                                                                          | What your store should do                                                                                         |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **200 OK**                    | Successful retrieval, update, or no-op                                                                           | Use response data                                                                                                 |
| **201 Created**               | Resource created (POST endpoints)                                                                                | Read `data.id` for the new resource                                                                               |
| **400 Bad Request**           | Validation failure or malformed input                                                                            | Show validation messages to the user; do **not** retry                                                            |
| **401 Unauthorized**          | Missing/invalid `x-auth-apikey` or `x-auth-signature`                                                            | Verify keys, signature, and canonical payload. **Do not retry blindly** — the same request will fail the same way |
| **403 Forbidden**             | Authenticated but not allowed (wrong holder, wrong NFT, deleted key)                                             | Check the resource belongs to this holder/key                                                                     |
| **404 Not Found**             | Resource doesn't exist or isn't visible to this caller                                                           | Don't retry; treat as legitimate "not there"                                                                      |
| **409 Conflict**              | State conflict (duplicate email on client create, double-spend on order, etc.)                                   | Reconcile state and re-attempt only after fixing                                                                  |
| **422 Unprocessable Entity**  | Semantically invalid (rare; mostly business-rule violations)                                                     | Surface `message`; don't retry                                                                                    |
| **429 Too Many Requests**     | Rate limited *(not currently enforced — see [03-environment.md § Rate limits](./03-environment.md#rate-limits))* | Back off exponentially when implemented                                                                           |
| **500 Internal Server Error** | Unexpected server failure                                                                                        | Retry once after a 1–2s delay; surface error to the user if it persists                                           |
| **502 / 503 / 504**           | Upstream / gateway issues (Envoy in front of the backend)                                                        | Retry with exponential backoff (max 3 attempts); these are typically transient                                    |

> **Spec note.** The live OpenAPI spec only formally declares `200`, `201`, `400`, and `404` responses. The other codes are returned by the global exception filter, NestJS's auth guards, and the upstream Envoy proxy, and they all conform to the same envelope. The enriched OpenAPI 3.1 spec in this repo declares all of them per-endpoint.

***

## Common failure modes (and how to diagnose them)

### `401 Unauthorized` — "Invalid signature"

By far the most common error during integration. Causes, in descending order of frequency:

1. **You signed a different string than you sent.** The signed payload must be byte-identical to the request body on the wire. If you `JSON.stringify` twice — once for signing, once for sending — JavaScript may produce different output between calls (object key ordering, etc.). **Stringify once, sign that string, send that string.**
2. **You signed without the right `Content-Type`.** Express body-parser only treats the body as JSON if `Content-Type: application/json` is set. Without it, the server reproduces the canonical payload as a raw string and your signature won't match.
3. **Wrong canonical payload for the HTTP method.** `GET` with query params signs `urlencode(query)`, not `""`. `GET` with route params only signs `JSON.stringify(routeParams)`. See [02-authentication.md](./02-authentication.md).
4. **Whitespace in your JSON.** The server expects compact JSON (no spaces between separators). `JSON.stringify(obj)` without a `space` arg gives you that in JS; `json.dumps(obj, separators=(",", ":"))` in Python; `json_encode($obj)` in PHP.
5. **API key was revoked.** Check the DAPP's keys page — keys are soft-deleted, so a revoked key returns 401, not 404.

### `401 Unauthorized` — "API key not found"

Your `x-auth-apikey` value doesn't match any active key for any holder. Either the key was never issued, was revoked, or you Base64-decoded/re-encoded it somewhere along the way and corrupted it. Compare byte-for-byte against the `apiKey` returned by `POST /keys`.

### `403 Forbidden` — on a `/dapp/clients/:id` endpoint

The client exists but doesn't belong to the NFT (Digital Key) currently set as your primary NFT. Either set the right NFT as primary via `PATCH /dapp/users/primary-nft`, or use a different API key tied to a different holder.

### `400 Bad Request` — `["countryCode must be a valid ISO 3166-1 alpha-3 country code"]`

The strain catalogue and several other endpoints expect ISO 3166-1 alpha-3 codes (e.g. `GBR`, `USA`, `DEU`) — **not** alpha-2 (`GB`, `US`, `DE`).

### `404 Not Found` — on a strain ID that you saw 30 seconds ago

Strains can be soft-deleted by the Dr Green admin team. They disappear from the catalogue immediately but the IDs persist in your local cache. Always handle 404s on strain lookups gracefully and refresh your cached catalogue.

### `502 / 503 Service Unavailable` — Envoy upstream errors

The backend sits behind an Envoy proxy on production. Transient 5xx with the message `upstream connect error or disconnect/reset before headers` is an Envoy → backend issue, not your problem. Retry with exponential backoff. If it persists more than 5 minutes, escalate to Dr Green.

***

## Retry guidance

| Scenario                                | Retry? | Strategy                                                    |
| --------------------------------------- | ------ | ----------------------------------------------------------- |
| Network timeout, connection refused     | ✅ Yes  | Exponential backoff: 1s, 2s, 4s, max 3 attempts             |
| `5xx` from Envoy/upstream               | ✅ Yes  | Same as above                                               |
| `429 Too Many Requests` (when enforced) | ✅ Yes  | Honour `Retry-After` if present, else exponential backoff   |
| `4xx` (other than 429)                  | ❌ No   | Fix the request; the same input will produce the same error |

### Idempotency

The Dr Green API does **not** currently support an `Idempotency-Key` header. This means:

* **Safe to retry on idempotent verbs:** `GET`, `PATCH` updates that set absolute values, `DELETE` of a specific ID
* **Risky to retry on:** `POST /dapp/orders` (will create a duplicate order), `POST /dapp/clients` (will fail with conflict on duplicate email — recoverable), `POST /dapp/sales` (duplicate sale)

For risky operations, prefer:

1. **Optimistic generation of a client-side correlation ID** stored in your DB before the request
2. **On any retry**, first `GET /dapp/orders?...` filtered by your correlation ID to see if the original succeeded
3. **Only re-`POST`** if you confirm the original didn't make it through

🔒 An `Idempotency-Key` header would meaningfully simplify store integration. Worth requesting from the Dr Green backend team.

***

## Logging recommendations

For every API call, log at minimum:

* HTTP method + path
* Response `statusCode` and `message`
* Response time
* A correlation ID you generated client-side

**Never log:**

* The full `secretKey`
* The raw `x-auth-signature` header value (it's deterministic for a given payload+key, so logging it is functionally equivalent to leaking part of the key's signing surface)
* Customer KYC fields (date of birth, ID document numbers, medical record content)

For 401/403 errors, log the `x-auth-apikey` so you can trace which key was rejected, but truncate to the first 16 chars (it's the public key, not catastrophic, but principle-of-least-disclosure applies).
