PPact
Social

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):

ProviderChar limitMedia
linkedin3,0009 images / 1 video
facebook63,20610 images / 1 video
instagram2,20010-image carousel
threads50010 images / 1 video
twitter2804 images / 1 video / 1 GIF
tiktok2,2001 video
youtube5,0001 video
bluesky3004 images
mastodon5004 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.
bash
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. 1

    Pick accounts

    Select one or more connected accounts. A disabled account rejects the post with 409 account_disabled.
  2. 2

    Write and preview

    Type the body; each selected channel renders a preview with its own counter.
  3. 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. 4

    Choose an action

    Save as draft, set a scheduled_for time, or set publish_now: true. Scheduling and immediate publish are mutually exclusive (the API returns 400 if both are sent).
  5. 5

    Publish

    A scheduled post publishes itself at its time. You can also publish a draft, a scheduled post, or a failed post now with POST /v1/social/posts/{public_id}/publish. If the provider refuses the post, it moves to failed and failure_reason says 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.