Head terms

Adding Social Publishing to Your SaaS

A practical build guide for shipping a "connect your social accounts and post" feature inside your own product: OAuth connection UI options, the actual API calls involved, and multi-tenant profile-group isolation between customers.

Quick answer

Adding social publishing to a SaaS means solving three problems: letting each customer connect their own accounts through OAuth (built yourself, via a hosted connection URL, or with your own provider app), making three backend calls — list profiles, create posts with an idempotency key, confirm delivery — and isolating tenants with one profile group and a group-bound API key per customer.

"Add social publishing" sounds like one feature, but it's really three separate problems stacked on top of each other: how a customer connects their own Instagram or LinkedIn account to your product without you ever touching their password, what your backend actually calls once that account is connected, and how you keep customer A's connected accounts from ever being reachable through customer B's session. This is a walkthrough of all three, with the actual request shapes involved — not a pitch, a build plan.

PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.

01

What are the options for the OAuth connection UI?

Every platform except a couple of credential-based ones (Bluesky uses an app password, Telegram a bot token, Mastodon an instance URL plus token) authenticates through OAuth, and each provider has its own consent screen, its own scope list, and its own developer-app approval process. Building that yourself means registering a developer app per platform, handling each provider's redirect and token-exchange flow, and — for Meta platforms and TikTok specifically — going through app review before your customers can connect real accounts. The alternative is a hosted connection URL: your backend creates a short-lived link, redirects the customer's browser to it, and gets them back automatically once they've authorized the account, with no provider OAuth code living in your own application at all. PostGate's hosted connection flow works this way, and a third option — bring-your-own provider app — lets you plug in your own registered developer-app credentials per platform if you need the consent screen and provider-side branding to be entirely your own, while still using the hosted flow underneath.

Registering your product origin, then starting a hosted connectionPostGate API
# once, from your backend
curl -X POST https://postgate.studio/api/connect-origins \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "origin": "https://app.example.com", "label": "Production SaaS" }'

# per customer, when they click "Connect Instagram"
curl -X POST https://postgate.studio/api/profiles/connect \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "instagram",
    "profile_group_id": "grp_acme123",
    "return_url": "https://app.example.com/settings/social"
  }'
# -> { "url": "https://postgate.studio/connect/eyJ...", "expires_in": 900 }
# redirect the customer's browser to that url
02

How do you read the connection result — success or cancellation?

When the customer finishes on the provider's consent screen, PostGate returns their browser to your return_url with query parameters describing what happened: connected (how many accounts got attached) and connection_platform on success, or connection_error and connection_platform if the customer canceled or the provider refused. Your settings page should handle both — a canceled connection is a normal outcome, not an error state worth alarming the customer over. From there, list the customer's profile group to render what's actually connected, including per-platform token health so you can surface a "reconnect Instagram" prompt before a scheduled post silently fails instead of after.

The redirect shape, both outcomesPostGate API
# success
https://app.example.com/settings/social?connected=1&connection_platform=instagram

# canceled or refused
https://app.example.com/settings/social?connection_error=access_denied&connection_platform=instagram
03

Which API calls does a "connect and post" feature actually need?

Past the connection screen, the feature reduces to three calls your backend makes on the customer's behalf: list their connected profiles to build a picker, create a post targeting the profile IDs they selected, and — for anything time-sensitive — read the post back (or, better, consume a webhook) to know whether it actually published. The create-post call takes a shared post body, a list of connected profiles, and optional media, and a stable Idempotency-Key so a retried request from your own backend after a timeout never becomes a duplicate post on the customer's real account.

Publish to whatever the customer connectedPostGate API
curl -X POST "https://postgate.studio/api/posts?profile_group_id=grp_acme123" \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: acme:job_88213" \
  -d '{
    "post": { "body": "New feature is live." },
    "profiles": ["pro_instagram123", "pro_linkedin456"],
    "media": ["https://cdn.example.com/launch.jpg"]
  }'
04

How do you keep tenants isolated from each other?

The part that's easy to get wrong isn't the OAuth flow — it's making sure customer A's connected Instagram account can never be published to through a request that's actually acting on behalf of customer B. PostGate models this with profile groups: one group per customer or brand, owning that customer's connected profiles, schedules, and posts as a hard publishing boundary. The strongest version of this isolation is a group-bound API key — a key created with a fixed group_id that cannot read or mutate another customer's data even if your application code has a bug and sends the wrong tenant identifier in a query parameter. If your architecture issues one API key per customer at connection time, tenant isolation stops depending on your own application logic being correct on every request and becomes something the API itself enforces.

One profile group and one bound key per customerPostGate API
curl -X POST https://postgate.studio/api/profile-groups \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Inc.",
    "timezone": "America/New_York",
    "brand": { "external_id": "tenant_842" }
  }'
# -> { "id": "grp_acme123", ... }

curl -X POST https://postgate.studio/api/api-keys \
  -H "Authorization: Bearer $POSTGATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme production publisher",
    "scopes": ["publish", "read"],
    "mode": "live",
    "group_id": "grp_acme123"
  }'
05

How do you make the connection flow feel like your product?

Customers expect "connect your Instagram" to feel native to whatever product they're already using, not like it hands them off to a third party mid-flow. PostGate's environments resource covers the branding surface — logo, primary color, and a verified custom domain where your plan enables it — so the hosted connection screen and its redirect chain can carry your product's name instead of PostGate's. This is a plan-gated feature, not a default, so check GET /api/platforms for what's actually enabled in your workspace before building UI around it.

Frequently asked questions

Do I have to build OAuth flows for every social platform myself?

No. Building it yourself means registering a developer app per platform, handling each provider's redirect and token-exchange flow, and passing app review for Meta platforms and TikTok. The alternative is a hosted connection URL: your backend creates a short-lived link, redirects the customer's browser, and gets them back once the account is authorized — no provider OAuth code in your application. A third option lets you bring your own provider-app credentials while still using the hosted flow underneath.

How do I keep one customer's social accounts isolated from another's?

Use one profile group per customer or brand — it owns that customer's connected profiles, schedules, and posts as a hard publishing boundary. The strongest isolation is a group-bound API key created with a fixed group_id: it cannot read or mutate another customer's data even if your application code has a bug and sends the wrong tenant identifier, so isolation is enforced by the API rather than your own request logic.

How do I know whether a customer completed or canceled the connection flow?

The redirect back to your return_url carries query parameters describing what happened: connected (how many accounts got attached) and connection_platform on success, or connection_error and connection_platform if the customer canceled or the provider refused. A canceled connection is a normal outcome, not an error state worth alarming the customer over — your settings page should handle both.

Can the social connection screen carry my own branding?

Yes, where your plan enables it. PostGate's environments resource covers the branding surface — logo, primary color, and a verified custom domain — so the hosted connection screen and its redirect chain carry your product's name instead of PostGate's. It is a plan-gated feature, not a default, so check GET /api/platforms for what is actually enabled in your workspace before building UI around it.

How do I avoid duplicate posts when my backend retries a request?

Send a stable Idempotency-Key header on the create-post call. A retried request from your own backend after a timeout then never becomes a duplicate post on the customer's real account — the API recognizes the key and returns the original result instead of publishing again.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs