# API Specifications

OpenAPI v3.1.0 specs organized by domain. Each YAML file is a self-contained spec for one resource area.

Inspired by Medusa (field selection, batch ops, error shape), Spree (include/eager-load, flat routing), and Whop (financial pipeline, idempotency), but grounded in our 153-table schema and existing BGC + BluntDashboard operational scenarios.

## Directory Structure

```
docs/api/
├── commerce/           Product, customer, order, payment, fulfillment, promotion, region, store
├── publisher/          Publisher, ad-server, attribution, membership, ledger, invoice, dispute
├── platform/           Identity, governance, macrodata
├── operational/        Connector, analytics, creative-ops, fulfillment-ops
└── webhooks/           Shopify, Stripe, platform-events (inbound)
```

## Worker Split

| Worker | Audience | Auth | Path Prefix |
|--------|----------|------|-------------|
| **Internal** | Staff, back-office | CF Access + IAM role binding | `/api/` |
| **Platform** | Publishers, customers, public | API key / OAuth / anonymous | `/v1/` |

Specs declare `x-worker: [internal]`, `x-worker: [platform]`, or `x-worker: [internal, platform]` at the operation level.

### Path key conventions

Two patterns are used depending on the domain's access model:

| Pattern | When | Example |
|---------|------|---------|
| **Separate paths** | Internal and platform return **different shapes** or different resource sets | `/products` (internal, full catalog) + `/v1/products` (platform, public catalog) |
| **Single path + annotation** | Both workers serve the **same shape** with scope filtering | `/publishers` with `x-worker: [internal, platform]` |

- **Commerce, platform, operational** specs use separate paths — platform-only paths carry an explicit `/v1/` prefix in the path key.
- **Publisher** specs use single paths — the `/v1/` comes from the server base URL, not the path key.

Both resolve to the same wire URLs (e.g., `https://platform.bluntcases.com/v1/products`).

## Conventions

| Convention | Detail |
|------------|--------|
| **Naming** | `snake_case` for all JSON property names and query/path parameters. Matches DB column naming. |
| **Identifiers** | `uid` with prefixed IDs (`pbl_xxx`, `ads_xxx`, `pub_xxx`). Path params use `{publisher_id}`, `{ad_server_id}`, etc. |
| **Pagination** | Cursor-based. Request: `?limit=20&cursor={opaque}`. Response: `{ data: [...], pagination: { limit, next_cursor, prev_cursor, has_more } }`. Cursor is an opaque base64-encoded token (typically `uid` of last item). First page omits `cursor`. |
| **Filtering** | Query params: `?phase=active&search=jane`. Phase filters map to `status->>'phase'`. |
| **Sorting** | `?order=-create_ts` — single param, `-` prefix for descending (Medusa convention). Default: `-create_ts`. |
| **Field selection** | `?fields=+variants,-annotations` — `+` adds to defaults, `-` removes from defaults, bare names replace defaults entirely (Medusa convention). |
| **Include relations** | `?include=publications,balances` — comma-separated relation names for eager-loading (Spree convention). |
| **Search** | `?search=jane` — full-text search across searchable fields. Separate from field-specific filters. |
| **Status JSONB** | Phase transitions via `PUT` with `{ status: { phase: "approved" } }`. Conditions appended server-side. |
| **Labels/Annotations** | Merge semantics: `PUT { labels: { "tier": "gold" } }` merges into existing labels. `DELETE` a key by setting it to `null`. |
| **Timestamps** | ISO 8601 with timezone. Fields: `create_ts`, `update_ts`, `archive_ts`. |
| **Errors** | `{ error: { code: "NOT_FOUND", type: "not_found", message: "Publisher not found", errors: [] } }`. `code` is machine-readable, `type` classifies the error category, `errors[]` carries field-level validation details. |
| **Idempotency** | `Idempotency-Key` header on all mutation endpoints (POST, PUT, DELETE). Server returns cached response for duplicate keys. Required on financial operations, optional elsewhere. |
| **Soft archive** | `DELETE /resource/{id}` sets `archive_ts`. `?include_archived=true` to include in lists. |
| **Workflow endpoints** | Verb-based: `POST /orders/{id}/fulfill`, `POST /publications/{id}/approve`. Marked with `x-workflow` extension. Body carries action-specific params. |
| **Batch operations** | `POST /resource/batch` with `{ create: [...], update: [...], delete: [...] }`. Response: `{ created: [...], updated: [...], deleted: { ids: [...], object: "resource" } }` (Medusa convention). |

## Endpoint Classification

| Type | Pattern | Example |
|------|---------|---------|
| **CRUD** | Standard resource operations | `GET /publishers`, `POST /publishers` |
| **Workflow** | Verb-based action triggers (`x-workflow`) | `POST /publications/{id}/approve` |
| **Batch** | Bulk create/update/delete | `POST /publishers/batch` |
| **Query** | Read-only aggregation | `GET /publishers/{id}/stats` |
| **Webhook** | Inbound with HMAC verification | `POST /webhooks/shopify` |

## Shared Components

Common schemas (`Pagination`, `Error`, `StatusJsonb`, `Labels`, `Annotations`), shared parameters (`Limit`, `Cursor`, `Order`, `Fields`, `Include`, `Search`, `IdempotencyKey`), and shared responses (`NotFound`) live in `_shared/components.yaml`. All domain specs reference them via `$ref: '../_shared/components.yaml#/components/...'`.

## Design Lineage

| Pattern | Source | Adaptation |
|---------|--------|------------|
| Cursor pagination | Industry standard | Opaque base64 cursor over `uid` |
| Field selection (`+`/`-`) | Medusa | Adopted directly |
| Include relations | Spree (JSON:API `include=`) | Adopted without JSON:API envelope |
| Batch CRUD | Medusa | `POST /resource/batch` with create/update/delete |
| Sort with `-` prefix | Medusa | Single `order` param replaces `sort` + `order` |
| Error shape with `type` | Medusa | Added `type` and `errors[]` to existing shape |
| Idempotency header | Whop/Medusa | Required on financials, optional elsewhere |
| Workflow extension | Medusa | `x-workflow` on action endpoints |
| Flat routing | Spree | Top-level resources with `?include=` over deep nesting |
