Composer
Draft one post, target multiple connected accounts, preview each channel against its real character and media limits, and rewrite the draft with your own Anthropic key — then save, schedule, or publish.
Composer
The composer is where a single idea becomes a post. You write the body once,
pick which connected accounts should receive it, watch a live per-channel
preview and character counter, optionally have AI tighten the copy, then save it
as a draft, schedule it, or publish immediately. The page is wired to the live
/v1/social API — it lists your accounts from GET /v1/social/accounts and
creates posts through POST /v1/social/posts.
Live surface
Account selection, per-channel limit enforcement, draft/schedule/publish, and
AI rewriting are all backed by real endpoints. The limits table is currently a
client-side map (web/src/app/(app)/social/content-limits.ts) pending a
content_limits field on the channel framework.
Per-channel limits
Because a 3,000-character LinkedIn post won't fit in a 280-character tweet, the
composer shows a side-by-side preview per selected channel with a live counter
and an over-limit warning. The current limits (from content-limits.ts):
| Provider | Char limit | Media |
|---|---|---|
| 3,000 | 9 images / 1 video | |
| 63,206 | 10 images / 1 video | |
| 2,200 | 10-image carousel | |
| threads | 500 | 10 images / 1 video |
| 280 | 4 images / 1 video / 1 GIF | |
| tiktok | 2,200 | 1 video |
| youtube | 5,000 | 1 video |
| bluesky | 300 | 4 images |
| mastodon | 500 | 4 images / 1 video / 1 audio |
Limits are a versioned client-side map
These figures reflect each network's public policy as of the map's last update.
When the channel framework exposes a canonical content_limits field, the
composer will read it live and this map is removed. Treat an over-limit warning
as advisory — the provider is the final authority at publish time.
AI rewrite
The composer's rewrite actions call POST /v1/social/composer/ai-improve, which
routes through the tenant's own Anthropic provider (BYOK). Modes
(_MODE_INSTRUCTIONS in api/routes/social.py):
improve— tighten grammar and phrasing, keep the author's voice, length within ~±15%.shorter— cut roughly 40%, drop filler, keep every concrete fact.punchier— sharpen the hook and energy without inventing claims.hashtags— leave the body unchanged, append 3–5 relevant hashtags.first_line— rewrite only the opening sentence, keep every other line verbatim.
curl -X POST https://api.pact.place/v1/social/composer/ai-improve \
-H "Authorization: Bearer $PACT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"shorter","content":"Our Q3 release ships today...","provider":"linkedin"}'
If the tenant has no Anthropic key configured, the endpoint returns 409 ai_not_configured so the UI can render a "Connect AI provider" call to action
rather than a generic error. The assistant is deliberately conservative: a
contentless draft (a lone emoji, random characters) is returned unchanged rather
than padded into a fabricated post, and the draft is fenced as data so text like
"ignore your instructions" is treated as post copy, not a command.
Saving, scheduling, publishing
- 1
Pick accounts
Select one or more connected accounts. A disabled account rejects the post with409 account_disabled. - 2
Write and preview
Type the body; each selected channel renders a preview with its own counter. - 3
Attach media (optional)
Paste image or video links under Attachments. The per-channel list says, before you schedule, whether each channel takes them: X takes up to 4 images, a GIF or a video; Bluesky up to 4 images (1 MB each); a YouTube post is one video (the first line becomes the title); LinkedIn, Facebook, Threads and Mastodon publish text only from Pact. When you schedule, Pact checks each file's real type and size and refuses what a network would reject. - 4
Choose an action
Save as draft, set ascheduled_fortime, or setpublish_now: true. Scheduling and immediate publish are mutually exclusive (the API returns400if both are sent). - 5
Publish
A scheduled post publishes itself at its time. You can also publish a draft, a scheduled post, or a failed post now withPOST /v1/social/posts/{public_id}/publish. If the provider refuses the post, it moves tofailedandfailure_reasonsays why.
A scheduled post sends itself, once
A background job checks every minute for posts whose scheduled_for time has
come and publishes each one through its connected account. You don't need to be
signed in. Every publish path claims the post first, so a scheduled post is
never sent twice, even when you press Publish now while the job is sending
it. See how posting works for retries and
failure reasons.
Some channels can't publish from Pact yet: TikTok (it needs TikTok's Content
Posting API approval), Instagram posts without an image, and a LinkedIn company
page with no organization author on record. A YouTube post needs one attached
video — each upload spends 1,600 of the Google project's 10,000 daily quota
units, and Google keeps videos uploaded by apps it hasn't audited private;
the composer says both before you schedule. The composer marks these channels drafts only and gives the
reason. The API refuses to schedule or publish to them with
422 {"code": "cannot_publish", "reason": "…"}. The API also refuses text that
is over a network's limit instead of cutting it, media sent to a network that
Pact publishes as text only, and any attachment it measured as the wrong type or
too large.