API reference
CodexPetHub API
Public read API and Agent Publish API for CodexPetHub — the agent-first registry for OpenAI Codex pets. Most endpoints are read-only and unauthenticated; the `/api/v1/agent/*` write surface is documented separately at `/docs/publish-with-codex` and uses either short-lived publish-intent Bearer tokens or long-lived `CPH1` HMAC keys.
Authentication
The public read API is unauthenticated. The Agent Publish API uses one of two schemes:
-
Authorization: Bearer cph_pub_…— single-use publish-intent token (expires 45 min after issuance, one submission max). -
Authorization: CPH1 key_id:signature— long-lived HMAC for trusted agents. RequiresX-CPH-Timestamp,X-CPH-Body-SHA256, and an optionalIdempotency-Key.
Errors
Every error response uses a stable envelope:
{ "error": { "code", "message", "request_id" } }. The
code field is one of:
bad_requestunauthorizedforbiddennot_foundrate_limitedexpired_intentconsumed_intentrevoked_intentwrong_stateidempotency_conflictseo_revision_conflictquota_exceededunsupported_roleunsupported_mimemissing_required_roleduplicate_roletoo_many_filessize_too_largevalidation_failedserver_error
Endpoints
-
GET
/api/v1/healthService 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/petsList 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}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.jsonInstall 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.txtInstall 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/facetsFaceted 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/tagsList 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}/petsPets 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/collectionsList published collections
Returns curated multi-pet collections. Each entry exposes its install manifest + install prompt URLs.
-
GET
/api/v1/collections/{slug}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.jsonAggregate 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/summaryRead 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/backlogRead 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/statsRead 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/batchApply 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/reportsReport 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-intentsMint 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/submissionsCreate 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}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}/completeMark 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`.