# CodexPetHub API

> Agent-readable API reference. Trust the live `/openapi.json` over this file when they disagree.

- OpenAPI: https://codexpethub.com/openapi.json
- Human page: https://codexpethub.com/api
- Version: 0.1.0
- License: MIT (MIT)

## Authentication

Two auth schemes exist; both apply only to the Agent Publish API and are not required for any read endpoint listed below.

- `Authorization: Bearer cph_pub_…` — single-use publish-intent token.
- `Authorization: CPH1 key_id:signature` — trusted-agent HMAC. The signature is base64-encoded HMAC-SHA256 of `METHOD\nPATH_WITH_QUERY\nTIMESTAMP\nBODY_SHA256`. Accompanied by `X-CPH-Timestamp`, `X-CPH-Body-SHA256`, optional `Idempotency-Key`.

## Errors

Every error response uses the `Error` envelope with one of these stable codes:

- `bad_request`
- `unauthorized`
- `forbidden`
- `not_found`
- `rate_limited`
- `expired_intent`
- `consumed_intent`
- `revoked_intent`
- `wrong_state`
- `idempotency_conflict`
- `seo_revision_conflict`
- `quota_exceeded`
- `unsupported_role`
- `unsupported_mime`
- `missing_required_role`
- `duplicate_role`
- `too_many_files`
- `size_too_large`
- `validation_failed`
- `server_error`

## Endpoints

### GET /api/v1/health

Tag: Meta

**Service health probe**

Returns `{ ok: true, version, time }`. Always public, never rate-limited. Used by uptime monitors and the install scripts to verify reachability.

### GET /api/v1/pets

Tag: Pets

**List published pets**

Cursor-paginated list of published pets. Drafts and hidden pets are never returned.

**Rate limit:** 30 requests/minute/IP for anonymous callers (Cloudflare WAF).

**Caching:** server-side KV cache keyed by query parameters (5 min); response carries `Cache-Control: public, max-age=60, s-maxage=300`.

### GET /api/v1/pets/{slug}

Tag: Pets

**Fetch a single pet's full metadata**

Returns the full pet record (metadata, asset URLs, manifest/prompt URLs, sprite states). Drafts and hidden pets return 404 to avoid leaking existence.

**Rate limit:** shares the 30/min/IP budget with `GET /api/v1/pets`.

### GET /api/v1/pets/{slug}/install-manifest.json

Tag: Pets

**Install manifest for a pet**

Server-rendered install manifest consumed by the `npx --yes codexpethub install <slug>` CLI (and any other agent that wants to install pets). The manifest is the only contract Codex uses to install a pet — it pins SHA-256 hashes for every file, locks the schema version, and asserts the security flags (`verify_hashes_required`, `execute_code: false`, `allow_external_urls_in_pet_json: false`). The `codexpethub` skill is publish-only and does not consume this endpoint.

**Caching:** `Cache-Control: public, max-age=300, s-maxage=3600`. KV-cached for 1 hour.

### GET /api/v1/pets/{slug}/prompt.txt

Tag: Pets

**Install prompt (plain-text alias)**

Convenience alias under `/api/v1/*` for the install prompt published at `/prompts/install/{slug}.txt`. Returns `text/plain; charset=utf-8` containing the multi-line “Copy for Codex” prompt the user pastes into Codex; the prompt instructs Codex to run `npx --yes codexpethub install <slug>` on the user's behalf, watch the SHA-256 verification line, and explain how to refresh custom pets in Codex Settings → Appearance → Pets.

**Caching:** `Cache-Control: public, max-age=300, s-maxage=3600`.

### GET /api/v1/facets

Tag: Pets

**Faceted filter counts for the current query**

Returns live per-axis counts (kinds, vibes, tags) plus a single `total` for the homepage and `/pets` filter toolbars. Each axis's counts apply every filter *except* its own, so the user can always switch within an axis without resetting their selection. `kinds` is pre-seeded with every allowed value (`creature`, `character`, `object`) so the toolbar renders a stable chip set even when an axis has zero pets.

**Rate limit:** shares the 60/min/IP budget with the other read endpoints under `/api/v1/`.

**Caching:** server-side KV cache keyed by the normalized query (5 min TTL); response carries `Cache-Control: public, max-age=60, s-maxage=300` and `x-cph-cache: hit|miss`.

### GET /api/v1/tags

Tag: Tags

**List all tags with pet counts**

Returns every tag attached to at least one published pet, with the number of pets per tag and links to the SSR + JSON listing endpoints.

**Caching:** `Cache-Control: public, max-age=300, s-maxage=3600`.

### GET /api/v1/tags/{tag}/pets

Tag: Tags

**Pets carrying a tag**

Returns the same shape as `GET /api/v1/pets?tag=…`, scoped to the path-parameter tag. Cursor-paginated, drafts hidden.

### GET /api/v1/collections

Tag: Collections

**List published collections**

Returns curated multi-pet collections. Each entry exposes its install manifest + install prompt URLs.

### GET /api/v1/collections/{slug}

Tag: Collections

**Fetch a single collection**

Returns collection metadata plus the ordered pet list. Each pet entry mirrors the top-level pet summary so the response is self-contained.

### GET /api/v1/collections/{slug}/install-manifest.json

Tag: Collections

**Aggregate install manifest for a collection**

Returns the same security envelope as a per-pet install manifest, but with an embedded `pets[]` array containing each pet's full install manifest in collection order.

**Caching:** `Cache-Control: public, max-age=300, s-maxage=3600`.

### GET /api/v1/analytics/summary

Tag: Analytics

**Read aggregated site analytics**

Returns a narrow, read-only Cloudflare Web Analytics summary for owner-controlled agents. Agents authenticate to this Worker with a dedicated analytics Bearer token; the Worker keeps the Cloudflare API token in secrets and returns only aggregate totals/top paths. The endpoint never exposes Cloudflare credentials, account settings, DNS, Workers, R2, D1, or write-capable APIs.

**Caching:** Worker KV caches the sanitized summary for 15 minutes. Response headers are `Cache-Control: no-store` so agent/client copies are not cached.

### GET /api/v1/agent/seo/backlog

Tag: Agent SEO

**Read the agent SEO backlog**

Returns a bounded queue of published pets whose SEO copy is new, weak, or stale. Requires a `CPH1` trusted-agent key with `pets:seo_read`. The endpoint never returns Cloudflare credentials or write-capable account data.

### GET /api/v1/agent/seo/stats

Tag: Agent SEO

**Read accepted SEO revision counts**

Returns factual accepted SEO revision counts from `pet_seo_revisions` for one UTC date. Requires a `CPH1` trusted-agent key with `pets:seo_read`; this endpoint is intended for health checks and runners that need an audit-table source of truth.

### POST /api/v1/agent/seo/batch

Tag: Agent SEO

**Apply a safe batch of pet SEO updates**

Updates only allowlisted SEO fields on published pets: description, tags, vibes, kind, and related slugs. Requires `CPH1` with `pets:seo_update`; publish-intent Bearer auth is rejected. The server validates descriptions, recalculates `search_text`, enforces `expected_seo_revision`, writes `pet_seo_revisions`, and invalidates public KV caches.

### POST /api/v1/reports

Tag: Abuse

**Report a published pet for abuse**

Public abuse-report intake. Body must include the pet slug, a reason from a closed enum, an optional details string (≤500 chars), and a Cloudflare Turnstile token (`expectedAction: "report"`).

**Rate limit:** 5 reports per hour per IP (Cloudflare WAF + Durable Object backstop).

**Auto-hide:** when a pet's `report_count` crosses the threshold while published, its status flips to `hidden` pending admin review. The response intentionally omits any signal of whether the auto-hide was tripped so reporters cannot probe the threshold.

Reporter identity is captured only as a peppered hash of the IP — raw IPs are never stored.

### POST /api/v1/publish-intents

Tag: Publish

**Mint a single-use publish intent (web flow)**

Public endpoint that issues a `cph_pub_…` Bearer token. The caller must present a valid Cloudflare Turnstile token (`expectedAction: 'publish_intent'`). The returned token is single-use: the active → consumed transition runs atomically when the first `POST /api/v1/agent/submissions` claims it, before the submission row is durably written, so a 5xx returned after consumption leaves the intent consumed and the caller must mint a fresh intent. Subsequent calls to `/complete` and `GET /agent/submissions/{id}` continue to authenticate against the originating submission until the 45-minute TTL elapses.

**Rate limit:** 1 active intent per IP, 3 per hour, 10 per day (Cloudflare WAF + Durable Object backstop). Only `CF-Connecting-IP` is trusted for IP attribution; `X-Forwarded-For` is ignored.

Does not authenticate; the client identifier is the Turnstile challenge solution.

### POST /api/v1/agent/submissions

Tag: Publish

**Create a submission and request upload URLs**

Creates a submission row and returns presigned R2 PUT URLs for the declared files. Authenticated either as a publish-intent Bearer token (single-use) or a `CPH1` HMAC key (long-lived). The body must declare exactly the files the agent intends to upload (`pet.json` + `spritesheet.webp`); the validator re-measures and re-hashes after upload.

**Single-use enforcement (publish-intent path):** the intent token is consumed atomically as part of this call — the active → consumed transition is the synchronization point that prevents two concurrent submissions from sharing one intent, and runs before the submission row is durably written. A 5xx returned after consumption leaves the intent consumed; callers retry with a fresh intent. A second `POST /api/v1/agent/submissions` with the same bearer returns `410 consumed_intent`.

**Idempotency:** trusted-agent callers MAY send `Idempotency-Key: <opaque>`; replays of the same body return the cached response. A different body for the same key returns `409 idempotency_conflict`.

**Quota:** trusted agents are subject to a per-key daily submission quota (default 100/day). Public publish-intent callers cap implicitly via the single-use token.

### GET /api/v1/agent/submissions/{id}

Tag: Publish

**Read submission status (owner-only)**

Returns the submission's current state, validator output (when available), and the public status URL. The caller must own the submission via the same auth credential used to create it; mismatched callers see `404 not_found` to avoid leaking submission existence.

### POST /api/v1/agent/submissions/{id}/complete

Tag: Publish

**Mark uploads complete and enqueue validation**

Transitions a submission from `awaiting_uploads` to `queued_validation` and enqueues a `validate_submission` message. For publish-intent callers, the intent token is already single-use — it is consumed atomically when `POST /api/v1/agent/submissions` first creates the submission, so a second `POST /api/v1/agent/submissions` with the same bearer returns `410 consumed_intent`. `/complete` itself accepts a consumed intent that owns this specific submission (the agent's normal upload → complete path); replaying `/complete` after success returns `409 wrong_state` because the submission has already moved past `awaiting_uploads`.

## Stability

The `schema_version` strings (`codexpethub.install.v1`, `codexpethub.collection.install.v1`) and the standard error codes are part of the contract. Breaking changes will only ship under a new `schema_version` and will preserve the legacy version for at least six months.
