Skip to main content
Base URL: http://localhost:8002 (local worker — the only supported deployment today). Status: Alpha. Surface area is broad but not contractually stable across releases. Last updated: 2026-09-28.

Jump to a group

Authentication

OAuth 2.1 Client Credentials grant. poindexter setup provisions the initial CLI client.

Tasks

Create, list, approve, and reject content tasks. The lifecycle is pending → in_progress → awaiting_approval.

Posts

Public post list + search, plus authenticated update / soft-delete.

Settings

Read and update app_settings rows over the REST surface.

Overview

Reference for the REST API exposed by the Poindexter worker. All routes are under /api/* — there is no URL-based versioning.
Stability warning. The API is not yet considered stable. Route shapes change between releases, especially around task metadata. Pin to a specific commit until the first tagged release.

API Principles

  • RESTful: Standard HTTP methods and status codes
  • JSON: All requests/responses in JSON format
  • Authentication: Bearer token in Authorization header
  • Pagination: Limit/offset for list endpoints

Authentication

OAuth 2.1 Client Credentials

All protected endpoints require an OAuth-issued JWT in the Authorization header:
poindexter setup provisions an initial CLI OAuth client on first run. Mint a fresh JWT for ad-hoc curl tests with:
Or POST against the token endpoint directly:
To register additional clients (brain daemon, MCP servers, scripts, OpenClaw, Grafana), run the matching poindexter auth migrate-* helper. The legacy static-Bearer fallback was removed in Glad-Labs/poindexter#249.

Development Bypass

When development_mode is true in app_settings, the literal token dev-token is accepted as a Bearer credential:
Never enable development_mode in production. As of the 2026-04-25 rotation, development_mode=false by default — the bypass is dormant unless an operator explicitly flips the flag.

Public Endpoints (no auth required)

  • GET /api/health — system health
  • GET /api/posts — published post list
  • GET /api/posts/{slug} — single post by slug
  • GET /api/categories — category list
  • GET /api/posts/search?q=... — post search

Core Endpoints

System Health

GET /api/health

Response (200 OK):
(services/task_executor.py was deleted 2026-05-16 in the Prefect Stage 4 cutover; dispatch is now Prefect-native and there is no task_executor component in the health payload.)

Tasks

GET /api/tasks

List tasks (requires auth). Supports ?status=pending&limit=20&offset=0.

POST /api/tasks

Create a content task.
The task enters the pipeline automatically: pending → in_progress → awaiting_approval (or rejected if QA fails).

POST /api/tasks/{task_id}/approve

Approve a task for publishing. Publishes immediately unless publish_at is set.

POST /api/tasks/{task_id}/reject

Reject a task with feedback.

Posts

GET /api/posts

List published posts. Public (no auth).

GET /api/posts/{slug}

Get a single post by slug. Public.

GET /api/posts/search?q={query}

Search posts by title/content. Public.

Settings

GET /api/settings

List all app_settings (requires auth).

PUT /api/settings/{key}

Update a setting.

Other Endpoints

Everything prefixed /api is the REST surface. Generated images (/images/generated/{filename}) and the server-rendered preview (/preview/{preview_token}) sit at the server root without the /api prefix. The posts RSS feed (/feed.xml) is served by the public site, not the worker. Video has no public list or stream endpoint. The video feed’s enclosures point at each approved video in object storage, and an operator streams a local render through the authenticated GET /api/media-approval/{post_id}/video/preview. The old /api/video/episodes list and stream routes were removed: they looked files up by post id, and renders are named by task id. The podcast feed likewise lists each approved episode with its URL in object storage, and an episode awaiting review streams through the authenticated GET /api/media-approval/{post_id}/podcast/preview. One podcast stream route remains: GET /api/podcast/episodes/{task_id}.mp3 serves the file a pipeline task rendered, which is how the operator console plays a podcast-type task. The GET /api/podcast/episodes list was removed for the same reason as the video routes. A preview reports a podcast or long-form video as ready once one is rendered for the post or its source task. It plays one only after it is approved and published to object storage: the same file the RSS feed serves.

Newsletter signup payload

POST /api/newsletter/subscribe takes email plus optional first_name and last_name. It stores those, a verified flag and a fresh unsubscribe token, and mirrors the signup into the Resend segment when one is configured. It does not record the caller’s IP address or user-agent. The public site does not call it: site signups go to Resend, and the worker pulls them from there. company, interest_categories and marketing_consent were retired on 2026-09-28. Nothing read them, so the worker no longer stores them and their columns are gone. A request that still sends any of them succeeds as before. The fields are ignored, the OpenAPI schema marks them deprecated, and the response carries two headers:
The warning names only the retired fields that were actually sent. Drop them from the caller to clear it.

Webhook and preview-token security

POST /api/webhooks/alertmanager requires Authorization: Bearer <token>: either an OAuth JWT (mint one with poindexter auth mint-grafana-token) or the static token in app_settings.alertmanager_webhook_token. A missing or mismatched token is rejected with 401; with neither configured the route fails closed with 503. Preview tokens (/api/posts/preview/{token}, /preview/{token}) are random 32-character hex strings. The pipeline mints one per task and stores it in the task’s metadata; older posts carry one in posts.preview_token. The token is the only credential. It does not expire, and it can only read, never modify state.

Error Responses

Common status codes: 400 validation, 401 missing/bad token, 404 not found, 500 server error.