# Errors

> HTTP status codes, machine-readable error codes, and the extra fields that accompany errors.

When a request fails, the response body has this shape:

```json
{
  "ok": false,
  "error": "similar",
  "message": "Benzer adlı firma var — mevcut kaydı seçin ya da confirm_new=1 ile yeni açın.",
  "similar": [ { "id": 7, "name": "Alize Paketleme San. ve Tic. A.Ş.", "…": "…" } ]
}
```

- **`error`** is a stable, machine-readable code — branch on it in your code.
- **`message`** is a Turkish description you can show to users; its wording may change, so don't parse it.
- Some errors carry extra fields: `similar`, `customer`, `gate_problems`, `module`, `state`, `otp`.

## Error codes

| Code | HTTP | Meaning |
|---|---|---|
| `unauthorized` | 401 | Missing, invalid, expired or revoked token. |
| `credentials` | 401 | Wrong username or password. |
| `otp_required` | 401 | Two-factor code required (`otp`). |
| `otp_invalid` | 401 | Wrong or expired two-factor code. |
| `forbidden` | 403 | You may not see / edit this record. |
| `insufficient_scope` | 403 | The OAuth token lacks `crm.write`. |
| `module` | 403 | The module is off in the licence or for the user. |
| `license` | 403 | The licence expired — admins only. |
| `ip` | 403 | Access from this network is not allowed (IP restriction). |
| `not_found` | 404 | Record not found. |
| `invalid` | 400 | Missing or invalid field (`message` explains). |
| `send` | 400 | The e-mail could not be sent (no mailbox, bad recipient…). |
| `exists` | 409 | A company with this name exists (returned in `customer`). |
| `similar` | 409 | Similar names exist (`similar`); send `confirm_new: true` to create anyway. |
| `gate` | 409 | Stage gate not met (`gate_problems`). |
| `locked` | 429 | Too many failed sign-ins — wait 60 s. |
| `rate_limited` | 429 | Per-minute request limit exceeded (`Retry-After`). |
| `limit` | 429 | Hourly question limit per user (40) reached. |

## HTTP status codes

| Code | Meaning |
|---|---|
| `200` | Success. |
| `201` | Created (OAuth client registration, SCIM user creation). |
| `204` | Success with no content (SCIM user deactivation). |
| `400` | Missing or invalid field. |
| `401` | Authentication failed — refresh the token or sign in again. |
| `403` | Not permitted — role, module, scope, license, or IP. |
| `404` | The record doesn't exist or is outside what you're allowed to see. |
| `405` | Method not supported (for example, `GET` on the MCP endpoint). |
| `409` | Business rule conflict — duplicate company, stage gate. |
| `429` | Rate limit — wait for the number of seconds in `Retry-After`. |
| `500` | Server error. If it keeps happening, send the request and the time to `info@solk.app`. |

## Example: duplicate company

```python
r = s.post(f"{BASE}/api/v1/customers", json={"name": "Alize Ambalaj"})
j = r.json()
if r.status_code == 409 and j["error"] == "exists":
    company = j["customer"]                     # already exists — use it
elif r.status_code == 409 and j["error"] == "similar":
    candidates = j["similar"]                   # let the user pick, or:
    j = s.post(f"{BASE}/api/v1/customers", json={"name": "Alize Ambalaj", "confirm_new": True}).json()
```

## Example: stage gate

```python
r = s.post(f"{BASE}/api/v1/opportunities/{oid}/stage", json={"stage": "Viable"})
if r.status_code == 409 and r.json()["error"] == "gate":
    for problem in r.json()["gate_problems"]:
        print("Missing:", problem)
```