PPact
Social

Social overview

Publish, schedule, monitor, and report on organic social across nine networks from one tenant-isolated workspace — the composer, calendar, inbox, listening, analytics, and compliance surfaces built on /v1/social.

Pact Social

Pact Social is the organic social-media surface of the platform: connect a workspace's LinkedIn, Meta, X, and other network accounts once, then draft, schedule, publish, monitor replies, listen for brand mentions, and report on performance — all inside the same multi-tenant, RBAC-enforced app that owns your CRM and marketing data. Every route lives under the /v1/social prefix and derives tenant_id from the auth context, so one workspace never sees another's accounts, posts, or engagements.

This surface is live

The composer, calendar, inbox, listening, analytics, and compliance flows are backed by real, tenant-isolated endpoints and Postgres tables (tenant_social_accounts, social_posts, social_engagements, social_mentions, tenant_listening_queries, social_listening_matches, social_scheduled_reports, and more). The one exception is the content Library, which currently persists to the browser — see below.

The surfaces

Supported networks

The provider registry (core/integrations/social/registry.py) ships factories for nine networks:

NetworkSlug(s)Notes
LinkedInlinkedinPersonal feed, company page, and share; full OAuth + client.
Metafacebook, instagram, threadsPage / IG / Threads posting via the Meta Graph client.
X (Twitter)twitterv2 API over OAuth 2.0 PKCE; requires a paid X Basic tier (~$100/mo).
BlueskyblueskyAT Protocol.
MastodonmastodonInstance-scoped.
TikToktiktokVideo posting.
YouTubeyoutubeVideo + description.

Each provider implements the same SocialProvider contract (core/integrations/social/base.py): authorize_url, complete_oauth, refresh_token, post, list_engagements, list_mentions, reply, analytics, and delete. A network only becomes connectable once an admin supplies its app credentials through the BYOK credentials wizard — the provider factory resolves them per-tenant at request time, so nothing is hardcoded.

How posting works

POST /v1/social/posts creates a post. Its status moves through the states in core/integrations/social/store.py:

  • draft: saved, not sent.
  • scheduled: has a scheduled_for time. The post shows on the calendar and publishes itself when that time comes.
  • publishing: claimed by a publisher and on its way to the network. A post in this state can't be claimed again, so the job and a person pressing Publish now can't both send it.
  • published: sent. provider_post_id and provider_url (the link to the live post) are kept on the post.
  • failed: not sent. failure_reason gives the network's reason in plain words. Try again publishes the post now.

publish_now: true publishes the post as soon as it's created. For a draft, a scheduled post, or a failed post, use POST /v1/social/posts/{public_id}/publish.

Scheduled publishing

The worker tick social.publish_due runs every minute. It finds scheduled posts that are due and publishes each one through the provider layer in core/integrations/social/publish.py. Before contacting the network, it commits its claim on the post. The rules below favor a late post over a duplicate post, because a duplicate is public and permanent:

  • Retries. Pact retries only when the network definitely did not receive the post: a rate limit (429) or a connection that never opened. It makes up to 3 attempts in total, waiting 2 and then 10 minutes, or as long as the network asks. While it waits, the post shows Retrying with the attempt number, the reason, and the time of the next try.
  • No automatic resend when the outcome is unknown. Pact won't resend a post if the network timed out after receiving it, or if Pact stopped mid-send and the claim is more than 10 minutes old. The post moves to failed with a reason telling you to check the network before you try again.
  • No late publishing. Pact won't publish a post on its own if it's more than 24 hours past its time, for example after an outage. The post moves to failed with a reason that says how late it is.
  • Paused workspaces. Posts in a suspended workspace, or one pending deletion, stay scheduled and aren't sent.
  • Sample data. Seeded sample posts are never sent to a network.

Scheduled analytics reports are a separate mechanism; see Social analytics.

Roles and audit

Any workspace member can create posts; connecting or disconnecting an account is restricted to admin/owner (the same policy as the credential store). Every meaningful action writes an audit event — social.account.connected, social.post.published, social.engagement.received, social.listening.alert.fired, and peers — and secrets never enter those payloads: only ids, kinds, names, and the connected account's display name.