← developers

loudly API — developer guide

loudly exposes a public REST API at /api/v1 so you can drive the daily loop programmatically — read the stack, propose and generate brand-grounded cards, run the pre-publish check, schedule, publish, and measure. It's the third thin adapter over the one service layer (lib/services/), alongside the app and the MCP server, so a key is bound by exactly the same authorization, plan gating, pre-publish severity, and credit rules as a person clicking in the UI (Rule 11 / PRD §12).


Authenticate

  1. Open Settings → Developers and generate a key. It's shown once (sk_live_…) — copy it and store it outside your repo. Each key carries read and/or write scope.
  2. Send it on every request as a Bearer token:
KEY=sk_live_YOURKEY
curl -s https://<your-domain>/api/v1/me \
  -H "Authorization: Bearer $KEY"

GET /api/v1/me echoes the workspace, plan tier, and active brand the key resolves to — call it first to confirm the connection. A key carries no user identity, so owner-only actions (team, billing, settings-as-owner) aren't reachable through the API by construction.


Quickstart — publish your first card

The daily loop, end to end. A brand-new workspace already arrives with proposals, so you can start at step 1; or propose your own from a one-line hook.

1. Read today's stack (or propose a card from a brief):

# the proposals + radar + queued/live items
curl -s https://<your-domain>/api/v1/today -H "Authorization: Bearer $KEY"

# or draft a brand-grounded proposal from a hook (the API writes the copy, not you)
curl -s -X POST https://<your-domain>/api/v1/cards \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"hook":"Weekend flat-white offer","channels":["instagram"]}'

2. Render the hero (1 credit). No body means "ensure the hero is rendered" — it's idempotent, so a repeat call returns the cached asset and charges nothing:

curl -s -X POST https://<your-domain>/api/v1/cards/$CARD_ID/asset \
  -H "Authorization: Bearer $KEY"

3. Pick where it lands (optional) — on Facebook and Instagram a card can publish as a feed post, a Story, or a Reel. The set_format action binds the placement per channel (missing = the feed default; Reels need a video creative — an image-only card is blocked by the pre-publish gate). The card echoes the choice in its formats map:

curl -s -X PATCH https://<your-domain>/api/v1/cards/$CARD_ID \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"action":"set_format","platform":"facebook","formatId":"fb-story"}'

4. Schedule itPATCH /api/v1/cards/{id} with the schedule action runs the pre-publish severity gate first (a block is a 422, an unacknowledged warning a 409):

curl -s -X PATCH https://<your-domain>/api/v1/cards/$CARD_ID \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"action":"schedule","at":{"date":"2026-07-05","time":"17:30"}}'

5. Or publish nowPOST /api/v1/cards/{id}/publish publishes every connected channel. Exactly-once: a re-publish returns alreadyPublished: true per channel rather than double-posting.

curl -s -X POST https://<your-domain>/api/v1/cards/$CARD_ID/publish \
  -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{}'

Endpoints

Every write needs a write-scoped key; reads need read. Publish and asset render are the sensitive tier (see rate limits).

Method + path Scope What it does
GET /api/v1/me read Who the key is — workspace, plan, active brand
GET /api/v1/today read Today's proposal stack (proposals + radar + queued/live)
GET /api/v1/cards read List cards; ?status=proposed|scheduled|posted|dismissed, cursor-paginated
POST /api/v1/cards write Propose a card from a one-line hook (AI-drafts the copy)
GET /api/v1/cards/{id} read One card in full
PATCH /api/v1/cards/{id} write Act on a card: editCopy · schedule · dismiss · backToStack · reschedule · repropose · set_format (what a channel publishes as — feed/Story/Reel on FB/IG)
POST /api/v1/cards/{id}/asset write Render or vary the hero (no steer/style = idempotent ensure-rendered, 0 credits on repeat; a steer and/or style override is a deliberate paid regen, 1 credit per call)
POST /api/v1/cards/{id}/publish write Publish now (runs the pre-publish gate; exactly-once per channel)
GET /api/v1/channels read Connected channels + health
GET /api/v1/performance read Top posts by uplift
GET /api/v1/posts/{id}/metrics read A post's engagement snapshot
GET /api/v1/credits read Credit meter (used / cap / balance)
GET /api/v1/brand-kits read List brand kits
GET /api/v1/brand-kits/{id} read A brand kit + its reference library (includes visualStyle)
PATCH /api/v1/brand-kits/{id} write Update curated kit fields, including visualStyle (preset is one of the eight registry styles — editorial-photo, flat-typographic-poster, flat-illustration, minimal-product, warm-lifestyle, matte-3d, bold-poster, paper-collage — or custom, the kit's derived contract)

The full request and response shape for every endpoint is in the OpenAPI spec at /api/v1/openapi — import it into Postman, Insomnia, or Bruno to explore it interactively.


Concepts

Pagination

List endpoints return { "data": [...], "nextCursor": "..." | null }. Pass ?cursor= (from the previous nextCursor) and an optional ?limit= (max 100, default 50). A null nextCursor means the last page.

Errors

Failures return a JSON { "error": "...", "report"?: {...} } body — the error is one of the codes below; report carries the pre-publish severity detail on blocked / needs_confirmation. The same catalog is in the spec at components/schemas/Error.

Status Codes
400 invalid_body, invalid_status
401 unauthorized
402 insufficient_credits
403 insufficient_scope, plan_limit, forbidden
404 not_found
409 no_active_brand, invalid_transition, needs_confirmation
413 payload_too_large
422 blocked
429 rate_limited

Rate limits

Per-key, per-minute ceilings by tier — reads 120, writes 30, sensitive (publish + generate) 10, scaled by plan (Growth ×2). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; a 429 adds Retry-After. A key driving the MCP server too draws down this same window — one blended budget per key. The credit ledger and idempotency are the real cost guards — this limiter is the secondary abuse guard.

Idempotency

What's not here yet

These are tracked and on the way — don't build against them until they ship:

Owner-gated capabilities (team, billing, settings-as-owner) are never exposed to a key — an agent or script is bound by exactly the same rules as a person in the UI.

Security

Keys carry your workspace's full read/write access — treat them like passwords, store them outside the repo or in a secret manager, and revoke a leaked key in Settings → Developers (revocation is immediate). Prefer a read-only key for anything that only queries.