# Hub error codes

Every tool / HTTP API uses the same envelope on failure:

```json
{
  "ok": false,
  "error": { "code": "E_VALIDATION", "message": "...", "details": "..." },
  "request_id": "req_...",
  "took_ms": 0
}
```

| Code | HTTP | Meaning |
|------|-----:|---------|
| `E_VALIDATION`    | 400 | Request body / params failed Zod schema. |
| `E_AUTH_MISSING`  | 401 | No `Authorization` / `X-API-Key` header (CRM API key) or no `Authorization: Bearer <ServiceJWT>` (Hub v1). |
| `E_AUTH_INVALID`  | 401 | Bad signature, malformed key, or jti replay. |
| `E_AUTH_EXPIRED`  | 403 | ServiceJWT `exp` passed (5-minute window). |
| `E_AUTH_REVOKED`  | 403 | API key revoked, or Connected App status != ACTIVE. |
| `E_SCOPE`         | 403 | API key / ServiceJWT lacks the scope this tool requires. |
| `E_NOT_FOUND`     | 404 | Subject (order, app, segment, ...) does not exist. |
| `E_CONFLICT`      | 409 | State transition not allowed (e.g. refunding a non-PAID order). |
| `E_IDEMPOTENCY`   | 409 | Same Idempotency-Key, different body. Generate a new key. |
| `E_SUPPRESSED`    | 422 | Send target is on the suppression list. |
| `E_RATE_LIMIT`    | 429 | Too many calls per (service, externalUserId) bucket. Retry after `retryAfter` seconds. |
| `E_DB_TIMEOUT`    | 504 | Read query exceeded `MCP_QUERY_TIMEOUT_MS`. |
| `E_DB`            | 503 | Underlying DB error (connection, deadlock, etc.). |
| `E_INTERNAL`      | 500 | Unhandled exception. Always reported with a request_id. |

## Payment-specific 409 codes

`POST /v1/pay/orders` and `POST /v1/pay/orders/{code}/refund` reuse the
same `E_CONFLICT` code with one of these `details.code` values:

| details.code | Meaning |
|---|---|
| `IDEMPOTENCY_KEY_CONFLICT`   | Same key, different body. |
| `REFUND_EXCEEDS_BALANCE`     | Requested refund > refundable balance. |
| `ORDER_NOT_REFUNDABLE`       | Order is not in PAID status. |
| `AMOUNT_MISMATCH`            | Hub-stored amount != requested amount during reconcile. |

`POST /v1/pay/orders` also returns **400 `BAD_AMOUNT`** when neither
`amount` nor `supplyAmount` is supplied, when `amount != supplyAmount + vat`,
or when the resolved total is out of range (1 .. 100,000,000). (ADR-006)

Virtual-account (`payMethod: "vbank"`) orders introduce the non-terminal
status `AWAITING_DEPOSIT` (계좌 발급됨, 입금 대기) and the redirect/notify
status `VBANK_ISSUED`. Completion arrives asynchronously via the deposit
webhook + the `payment_notify_url` server-to-server notification.

## Account linking

`401 ACCOUNT_LINK_REQUIRED` is a special case — the body includes
`link_url` for the user to consent at `hub.08liter.com/connect`.
After they consent once, subsequent calls resolve silently.
