open-dispatch

Reference

API Reference

Once Open-Dispatch is running, the API is available at http://localhost:8000. No auth is required by default — it's designed for trusted self-hosting. Front it with Cloudflare Access, Tailscale, or basic auth if you expose it to the internet.

Confirm it's running

Run this first. You should see {"status":"ok"}.

curl http://localhost:8000/healthz
GET/healthz

Returns JSON for API clients, the HTML dashboard for browsers.

Request

curl http://localhost:8000/healthz

Response

{"status": "ok"}
POST/dispatch

Enqueues a ContentUnit. Returns a queue row ID you can poll.

Request

curl -X POST http://localhost:8000/dispatch \
  -H "Content-Type: application/json" \
  -d '{
    "targets": ["twitter", "bluesky"],
    "formats": {
      "twitter_thread": {
        "tweets": ["your tweet here"]
      },
      "bluesky_post": {
        "text": "your post here"
      }
    }
  }'

Response

{"id": "abc123", "status": "queued", "targets": ["twitter", "bluesky"]}
GET/queue

Filter by status: queued, publishing, published, failed, dead.

Request

curl "http://localhost:8000/queue?status=published"

Response

[{"id": "abc123", "status": "published", "platform": "twitter", ...}]
GET/queue/{id}

Content-negotiated — JSON for API clients (Accept: application/json), HTML detail page for browsers.

Request

curl -H "Accept: application/json" http://localhost:8000/queue/abc123

Response

{"id": "abc123", "status": "published", "post_id": "1234567890", ...}
POST/queue/{id}/retry

Resets the row to queued so the worker picks it up again.

Request

curl -X POST http://localhost:8000/queue/abc123/retry

Response

{"id": "abc123", "status": "queued"}
DELETE/queue/{id}

Permanently removes the row. Cannot be undone.

Request

curl -X DELETE http://localhost:8000/queue/abc123

Response

{"deleted": true}
POST/ai/adapt

Takes a source caption and returns platform-optimised versions. Uses Ollama → OpenRouter → heuristic fallback.

Request

curl -X POST http://localhost:8000/ai/adapt \
  -H "Content-Type: application/json" \
  -d '{
    "text": "We just shipped v0.4 — self-host free, MIT.",
    "platforms": ["twitter", "linkedin", "instagram"]
  }'

Response

{"twitter": "v0.4 is live. self-host free, MIT ↓", "linkedin": "...", "instagram": "..."}
POST/media/transcode

Sends back the resized image as binary. Accepts image/* body.

Request

curl -X POST "http://localhost:8000/media/transcode?platform=twitter" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg \
  --output photo.twitter.jpg

Response

(binary JPEG)
GET/media/specs

Returns dimensions, aspect ratio, and format for all 10 built-in specs.

Request

curl http://localhost:8000/media/specs

Response

{"twitter": {"width": 1600, "height": 900, "format": "JPEG"}, ...}

ContentUnit — full shape

{
  "category":      "general",          // optional label
  "targets":       ["twitter:work", "bluesky", "threads"],
  "scheduled_for": "2026-06-26T18:00:00+00:00",  // omit to post immediately
  "formats": {
    "twitter_thread":   { "tweets": ["t1", "t2"], "media_paths": [] },
    "bluesky_post":     { "text": "…", "images": [{"path": "…", "alt": "…"}] },
    "telegram_message": { "text": "…", "photo_path": "…", "parse_mode": "HTML" },
    "instagram_post":   { "caption": "…", "image_url": "https://…" },
    "linkedin_post":    { "text": "…", "asset_urn": "urn:li:digitalmediaAsset:…" },
    "threads_post":     { "text": "…", "image_url": "https://…", "video_url": "https://…" }
  },
  "webhook_url": "https://example.com/callback"  // optional, fires on publish/fail
}

Target syntax: platform[:account]. Per-account env vars are <PLATFORM>_<FIELD>_<ACCOUNT> (uppercase). twitter:work resolves to TWITTER_ACCESS_TOKEN_WORK.

Something missing or broken?

Open an issue