# Content API

General-purpose JSON API for creating and publishing site content — usable by AI agents, scripts, or any HTTP client — blog articles
and scheduled social posts. Clients are trusted: they may create drafts,
publish immediately, or schedule.

**Auth — bearer/refresh model.** An admin creates a named API client in
`/admin/settings` and receives its **refresh token** (`ctxr_...`, shown
once, stored hashed). Exchange it for a 1-hour **access token**:

```bash
ACCESS_TOKEN=$(curl -sX POST https://ctxshift.com/api/auth/token \
  -H "Authorization: Bearer $REFRESH_TOKEN" | jq -r .access_token)
```

Every other endpoint takes `Authorization: Bearer $ACCESS_TOKEN`. Expired
access tokens 401 — just exchange again. Revoking the client in admin kills
its refresh token and outstanding access tokens immediately. With no
clients provisioned, the API is effectively disabled.

**Base URL:** `https://ctxshift.com/api`

**Machine-readable spec:** [`/api/openapi.json`](https://ctxshift.com/api/openapi.json)
(OpenAPI 3) — exact request/response shapes; this page is the narrative version.

## Endpoints

### GET /context

Drafting context: all projects, tags, and the 20 most recent articles
(any status) for voice and dedup reference.

### POST /articles

```bash
curl -sX POST https://ctxshift.com/api/articles \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Context Engineering for Long-Running Agents",
    "slug": "context-engineering-long-running-agents",
    "content_md": "## The problem\n\nMarkdown body here...",
    "excerpt": "What actually keeps an agent on task.",
    "status": "draft",
    "project_id": 1,
    "tag_ids": [1, 3]
  }'
```

- `content_md` renders to HTML server-side (same MDEx pipeline as admin).
- `status`: `draft` (default) | `published` | `scheduled` (+ `scheduled_for`
  ISO8601 UTC). Scheduled articles auto-publish via the minutely cron.
- Returns `201` with the article JSON including its public `url`.
- Validation failures return `422` with per-field errors.

### PATCH /articles/:id

Same fields as create; omitted fields keep their values. Omitting `tag_ids`
keeps existing tags.

### GET /articles?status=draft

List articles, optionally filtered by status (metadata + excerpt).

### GET /articles/:id

Full article: markdown source, rendered HTML, tags, project link,
reading time, view count.

### GET /projects · GET /projects/:id · POST /projects

All projects (any visibility) with tags, descriptions, and repo/watch
URLs. Create with `title` (slug generated when omitted); `visibility`
defaults to draft, `scheduled` requires `scheduled_for`.

### GET /videos · GET /shorts

YouTube-mirrored videos and shorts, with titles, stats, project links,
and site URLs — useful for linking `video_id` on social posts.

### GET /tags

All tags (id, slug, name).

### POST /social-posts

```bash
curl -sX POST https://ctxshift.com/api/social-posts \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "New post: how I structure agent context on real projects.",
    "status": "scheduled",
    "scheduled_for": "2026-08-01T15:00:00Z",
    "article_id": 42,
    "media_urls": ["https://ctxshift.com/images/diagram.png"]
  }'
```

- `status`: `draft` (default) | `scheduled` (+ `scheduled_for`).
- Linking `article_id` / `project_id` / `video_id` appends that content's
  public URL to the outgoing post.
- `media_urls` must be publicly fetchable images; they're mirrored to X's
  media endpoint at post time.
- Max 280 characters (the appended link is extra — X shortens links to 23
  chars regardless of length; keep content under ~250 when linking).

### DELETE /social-posts/:id

Deletes a draft, scheduled, or failed post. Posts already live on the
platform are refused (409) — they're timeline history.

### GET /social-posts?status=scheduled

List composed posts by status (`draft` | `scheduled` | `posted` | `failed`).
Failed posts carry the `error` message.

## Publishing pipeline

- `CTXShift.Workers.PublishDueContent` (cron, minutely): publishes due
  scheduled articles/projects, enqueues due social posts.
- `CTXShift.Workers.PostSocial`: posts to X (OAuth 1.0a, v2 API), 3
  attempts, then marks the post `failed` with the error.
- `CTXShift.Workers.RefreshSocialMetrics` (cron, hourly): pulls
  likes/reposts/views back for the site timeline.

The Fly machine is always-on (`auto_stop_machines = "off"`), so cron ticks
are reliable.

## Secrets

API credentials are created in `/admin/settings`, not env vars. Fly
secrets needed for posting:

| Secret | Purpose |
|---|---|
| `X_CONSUMER_KEY` / `X_CONSUMER_SECRET` | X app credentials |
| `X_ACCESS_TOKEN` / `X_ACCESS_TOKEN_SECRET` | X user-context tokens |
