# ScytheRadar API — Integration Contract

**Status: FROZEN** · Applies to every path under `/api/v1/`
Machine-readable spec: [`/openapi.yaml`](openapi.yaml) (OpenAPI 3.1)

## 1. Versioning promise

| Rule | Commitment |
|---|---|
| **Path freeze** | Existing `/api/v1/*` paths, HTTP methods and parameter names will **not change**. |
| **Additive-only fields** | Within v1, a response field is **never removed or renamed**. We only add fields. Clients **must tolerate unknown fields**. |
| **New endpoints** | New endpoints and new **optional** request fields may be added at any time. |
| **New enum values** | `verdict`, `severity`, `flags` may gain values. Treat unknown values as `unknown`, never as an error. |
| **Breaking changes** | Require a new major prefix (`/api/v2/`). **v1 keeps working**; no forced migration. |
| **Deprecation** | Any future removal ships with a deprecation window and `Sunset`/`Deprecation` response headers before it takes effect. |

**Integrator rule of thumb:** parse defensively, ignore unknowns, never hard-fail on an
unrecognised enum.

## 2. Authentication

- `X-API-Key: <key>` header, **or** `?key=<key>` query parameter (header wins).
- Keyless exception: `GET /api/v1/demo/risk` (6 calls / IP / day) and the public
  `GET /api/v1/flags`, `GET /api/v1/plans`.
- Missing/invalid key → `401 { "error": "missing_api_key" }`.

## 3. Errors — flat envelope

```json
{ "error": "quota_exceeded", "hint": "…human text…", "code": 429 }
```

| HTTP | `error` | Meaning |
|---|---|---|
| 400 | `bad_*` (`bad_chain`, `bad_tx_hash`, `bad_amount`, `missing_*`) | Invalid parameters |
| 401 | `missing_api_key` | No / bad key |
| 403 | `plan_forbidden` | Your plan does not include this endpoint |
| 422 | `idempotency_key_reuse` | Same idempotency key, different request |
| 429 | `quota_exceeded` | Daily quota exhausted (Beijing midnight reset) |

> **Business verdicts are never errors.** A "money is dirty" outcome is `200` with
> `data.verdict != "clean"` (screening) or `data.passed == false` (policy). We never signal
> a dirty result with a 4xx.

## 4. Idempotency

Send `Idempotency-Key: <uuid>` (header) **or** `request_id` (body).

- Replay of the same key → cached response, header `Idempotency-Replayed: true`,
  **no extra quota consumed**.
- Reuse with a *different* request → `422 idempotency_key_reuse`.

## 5. Rate limiting

Every keyed response carries:

```
X-RateLimit-Limit:     2000
X-RateLimit-Remaining: 1999
X-RateLimit-Reset:     1760006400      # unix seconds, Beijing-time midnight
X-RateLimit-Policy:    screen;w=86400
```

Agents should **stop or back off** when `Remaining` hits `0` — do not retry in a tight loop.

## 6. Stability of `/api/v1/*` — what counts as breaking

**Breaking (needs `/api/v2/`):** removing a path, removing/renaming a field, changing a
field's type, making an optional field required, changing a default that alters results.

**Non-breaking (may ship any time):** adding an endpoint, adding an optional request field,
adding a response field, adding an enum value, adding an error `hint`.

---

_Contact: https://scytheradar.com/b2b.html · Terms: https://scytheradar.com/terms.html_
