# Agent guide — Afropenda Marketing Bank

Everything the dashboard shows is an endpoint here, on the same authorization boundary. An agent with a
key carrying the right scopes can read the bank, open and close tests, and (with write scopes) add lines,
claims, angles and labels — without a browser. The REST surface *is* the agent surface.

## Connect

| | |
|---|---|
| Base URL | `https://marketing.afropenda.com` |
| Auth | `X-Api-Key: afpmb_live_…` — or `Authorization: Bearer afpmb_live_…` |
| Contract | `GET /api/openapi.json` (OpenAPI 3.1; `x-required-scopes` on every operation) |
| This guide | `GET /api/doc` |

Start with `GET /v1/me` (no scope): it returns the key's scopes, its store pin and the stores it can see.
A pinned key is refused on every workspace-level path (`/v1/stores`, `/v1/users`…) whatever its scopes.
`{sid}` accepts the store id or its slug (`afropenda`). `401` = unknown / revoked / expired key.
`403` = missing scope or store not granted. `404` = the thing does not exist (checked before the grant).
Rate limit: 120 requests / minute / key (`429` + `retry-after`).

## The one call before writing for a line

`GET /v1/stores/{sid}/lines/{line}/brief` (add `?format=md` for markdown). It returns, for that line only:

- **angles** — every angle measured on this line, **per surface** (`body-copy` = what the primary text
  argues · `video-content` = what the video actually argues · `static`), each with all-ads ROAS,
  PASS/judged, LOSS and a verdict, beside the brand-wide cell for the same surface. Surfaces agree only
  about half the time; never average them.
- **untested_angles** — proven brand-wide, never run on this line.
- **pillars** — USP pillars ranked by **independent people** (person_key, never rows), each with the
  verbatims an ad MAY quote (`quotable`) and a count of research-only quotes it may not.
- **objections** — what customers complain about, with `never_say`. These exist to be avoided; do not
  write them into copy unless the owner asks (rule of 4/11/2026).
- **claims** — `allowed` / `forbidden` / `needs_owner`, line-specific and brand-wide.
- **quotes_shareable**, **themes**, **health** (quotes, people, shareable, sources live/stale/blocked).

Gate used everywhere: judged = spend ≥ $1000; PASS = ROAS ≥ 1.4 and ≥ 10 orders;
LOSS = ROAS < 1; account break-even ≈ 1.3. Always report all-ads ROAS + pass rate, never PASS-only ROAS.

## Rules carried by the data

1. `shareable=false` quotes are research-only — read them, never quote them in an ad.
2. Count people by `person_key`; one customer can have 17 submissions.
3. A verbatim that carries a name was dropped, not edited (`excluded`, `exclude_reason`).
4. Angles carry a `surface`; cells of different surfaces are never summed.
5. Objections are for avoiding.

## Tests — the loop

`POST /v1/stores/{sid}/tests` when you pull an angle out to run (angle_code, line_key, surface,
hypothesis, offer, ad_set_id, ad_ids). `PATCH` it with spend / purchases / roas as it runs; close it
with `closed_at` and a `learning` — the verdict follows the gate unless you set one.

## Scopes

- `read:stores` — list / read stores
- `write:stores` — create / update stores
- `read:voc` — quotes, people, lines, pillars, objections, claims, briefs
- `write:voc` — import or edit VOC rows, claims
- `read:angles` — angles, ads, angle × line tables
- `write:angles` — import or edit angles / ad labels
- `read:tests` — read the test log
- `write:tests` — open, update and close tests
- `read:api_keys` — list keys
- `write:api_keys` — mint, rotate, revoke keys no wider than your own
- `read:users` — list people
- `write:users` — invite / change people and their store grants
- `read:audit_log` — read the audit log
- `admin` — every scope, including users and keys

`admin` satisfies every check; `write:*` never implies `read:*`. A key is minted no wider than its
minter; staff can only mint keys pinned to a granted store.

## Endpoints

| route | operationId | scope | what |
|---|---|---|---|
| `GET /v1/me` | `get_me` | – | Who am I: scopes, store pin, visible stores. Start here. |
| `GET /v1/scopes` | `list_scopes` | – | The scope catalogue. |
| `GET /v1/stores` | `list_stores` | `read:stores` | Stores this key can reach (refused for a pinned key — use get_me). |
| `POST /v1/stores` | `create_store` | `write:stores` | Add a store (slug, name, shop_domain). |
| `GET /v1/stores/{sid}` | `get_store` | `read:stores` | One store with its lines, sources and counts. {sid} = id or slug. |
| `PATCH /v1/stores/{sid}` | `update_store` | `write:stores` | Rename / change status. |
| `GET /v1/stores/{sid}/lines` | `list_lines` | `read:voc` | Product lines with VOC and ad counts. |
| `GET /v1/stores/{sid}/lines/{line}` | `get_line` | `read:voc` | One line's registry row. |
| `PUT /v1/stores/{sid}/lines/{line}` | `upsert_line` | `write:voc` | Create or edit a line (display, patterns, sizes, fabrics, default offer, status). |
| `GET /v1/stores/{sid}/lines/{line}/brief` | `get_line_brief` | `read:voc` | THE one call before writing for a line: angles per surface on this line, pillars with quotable verbatims, objections to avoid, claims, shareable quotes, data health. ?format=md for markdown, ?quotes=N. |
| `GET /v1/stores/{sid}/quotes` | `search_quotes` | `read:voc` | Quotes. Filters: line, theme, topic, source, field, person, rating_min, rating_max, shareable=0|1, q (full text), limit, offset, include_excluded=1. |
| `GET /v1/stores/{sid}/themes` | `list_themes` | `read:voc` | Theme coverage by independent people (?line=). |
| `GET /v1/stores/{sid}/people/summary` | `people_summary` | `read:voc` | How many independent people, by source overlap. |
| `GET /v1/stores/{sid}/pillars` | `list_pillars` | `read:voc` | Generated USP pillars (?line=). |
| `GET /v1/stores/{sid}/objections` | `list_objections` | `read:voc` | Objections to AVOID in copy (?line=). |
| `GET /v1/stores/{sid}/claims` | `list_claims` | `read:voc` | Claims allowed / forbidden / needs-owner (?line=, ?status=). |
| `POST /v1/stores/{sid}/claims` | `create_claim` | `write:voc` | Record a claim decision. |
| `PATCH /v1/stores/{sid}/claims/{id}` | `update_claim` | `write:voc` | Change a claim's status / wording. |
| `DELETE /v1/stores/{sid}/claims/{id}` | `delete_claim` | `write:voc` | Remove a claim. |
| `GET /v1/stores/{sid}/angles` | `list_angles` | `read:angles` | Angle registry with brand-wide cells per surface. |
| `PUT /v1/stores/{sid}/angles/{code}` | `upsert_angle` | `write:angles` | Add or rename an angle (A24…). |
| `GET /v1/stores/{sid}/angles/{code}` | `get_angle` | `read:angles` | One angle: cells per line × surface and the ads behind them (?line=, ?surface=). |
| `GET /v1/stores/{sid}/angle-lines` | `angle_line_matrix` | `read:angles` | angle × line × surface cells (?line=, ?surface=, ?min_judged=). |
| `GET /v1/stores/{sid}/ads` | `list_ads` | `read:angles` | Ads with their angle labels (?line=, ?angle=, ?format=, ?video_id=, ?min_spend=, ?limit=). |
| `PUT /v1/stores/{sid}/ads/{ad_id}/angle` | `label_ad` | `write:angles` | Hand-label one ad on one surface. |
| `GET /v1/stores/{sid}/videos/{video_id}` | `get_video` | `read:angles` | A video's content labels, metrics and the ads that ran it. |
| `GET /v1/stores/{sid}/tests` | `list_tests` | `read:tests` | Tests (?verdict=open|pass|be|loss|killed, ?angle=, ?line=). |
| `POST /v1/stores/{sid}/tests` | `open_test` | `write:tests` | Pull an angle out to test: angle_code, line_key, surface, hypothesis, offer, started_at, ad_set_id, ad_ids. |
| `GET /v1/stores/{sid}/tests/{id}` | `get_test` | `read:tests` | One test. |
| `PATCH /v1/stores/{sid}/tests/{id}` | `update_test` | `write:tests` | Update numbers, close with verdict + learning (closed_at + spend ⇒ verdict from the gate when not given). |
| `GET /v1/api-keys` | `list_api_keys` | `read:api_keys` | Keys (never secrets). |
| `POST /v1/api-keys` | `create_api_key` | `write:api_keys` | Mint a key: name, scopes, store_id (pin), expires_in_days. Secret returned once. |
| `POST /v1/api-keys/{id}/rotate` | `rotate_api_key` | `write:api_keys` | Replace a key in use; grace_seconds keeps the old secret valid. |
| `POST /v1/api-keys/{id}/revoke` | `revoke_api_key` | `write:api_keys` | Revoke immediately. |
| `GET /v1/users` | `list_users` | `read:users` | People and their store grants. |
| `POST /v1/users` | `create_user` | `write:users` | Invite a person (email, password, role, store_ids). |
| `PATCH /v1/users/{id}` | `update_user` | `write:users` | Change role / grants / password / disabled. |
| `GET /v1/audit-log` | `list_audit_log` | `read:audit_log` | Who changed what (?store=, ?limit=). |
