Backend integration

This is the recommended way to integrate PostFuze into a multi-user product: register a BYOK OAuth app per platform, drive the connect flow from your backend, persist the account IDs PostFuze returns against your own users, then publish and stay in sync via webhooks. Your PostFuze API key lives only on your server.

Architecture at a glance

  1. Register OAuth apps once per platform with POST /social-networks (BYOK).
  2. Connect accounts per end-user: get an auth URL, redirect, and store the returned social account IDs against your tenant_id.
  3. Create posts targeting those account IDs.
  4. Sync state via webhooks (preferred) or polling.

Throughout, pass your own user identifier as tenant_id so PostFuze tags every connected account and import job with it. That lets you filter accounts per user later without keeping a separate mapping in sync — though storing the IDs yourself (below) is still recommended for speed and joins.

1
Your backend
Call POST /posts with your API key
Send the content and the accounts to publish to. PostFuze responds 201 Created right away with the post and its targets.
2
PostFuze
Publishes to each connected account
Each target is delivered to its platform independently, with token refresh, media upload, and retries handled for you.
3
PostFuze → your webhook
Signs a webhook back to you
Once the post settles, PostFuze sends an HMAC-signed event with the per-account result — no polling required.

Step 1 — Register a BYOK app

PostFuze uses your platform developer-app credentials. Register them once per platform; the upsert key is (org, platform), so re-posting updates them in place. The client_secret is encrypted at rest and never returned.

curl
curl https://api.postfuze.com/api/v1/social-networks \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "linkedin",
    "display_name": "Acme LinkedIn App",
    "client_id": "YOUR_LINKEDIN_CLIENT_ID",
    "client_secret": "YOUR_LINKEDIN_CLIENT_SECRET",
    "redirect_uri": "https://api.postfuze.com/api/v1/connect/callback",
    "scopes": ["r_basicprofile", "w_member_social"]
  }'

Set the platform's OAuth redirect URI to PostFuze's callback: https://api.postfuze.com/api/v1/connect/callback. PostFuze handles the code exchange and token storage for you. See Social networks for the full field list.

Step 2 — Connect an account and store the ID

When a user wants to connect, request an auth URL and include your own user identifier as tenant_id plus the redirect_uri you want PostFuze to bounce back to.

POST /social-networks/{platform}/auth-url
curl https://api.postfuze.com/api/v1/social-networks/linkedin/auth-url \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uri": "https://yourapp.com/integrations/postfuze/return",
    "tenant_id": "user_42"
  }'
200 OK
{ "auth_url": "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=…&state=…" }

Redirect the user to auth_url. After they approve, PostFuze's callback exchanges the code and bounces back to your redirect_uri with one of two outcomes:

Query paramsMeaningWhat to do
?status=connected&account_id=…A single account was connected immediately.Store account_id against the user.
?status=pending&session=…Several pages/accounts are available (e.g. Facebook Pages or Google Business locations).Run the page-selection flow below.

For the pending case, fetch the available pages and finalize the user's selection:

curl
# List available pages for the pending session
curl https://api.postfuze.com/api/v1/social-accounts/pending/SESSION_TOKEN \
  -H "Authorization: Bearer sk_live_…"

# Finalize one or more selections
curl https://api.postfuze.com/api/v1/social-accounts/pending/SESSION_TOKEN/finalize \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "selectedPageIds": ["1784523…", "1784524…"] }'
200 OK — pending
{
  "network": "facebook",
  "availablePages": [
    { "id": "1784523…", "type": "page", "name": "Acme Co", "username": "acme", "profilePictureUrl": "https://…" },
    { "id": "1784524…", "type": "page", "name": "Acme EU", "username": "acme_eu", "profilePictureUrl": "https://…" }
  ]
}
200 OK — finalize
{
  "connectedAccounts": [
    { "id": "a1b2c3d4-…", "nickname": "acme", "username": "acme", "network": "facebook", "accountType": "page" }
  ]
}

Persist the social account IDs (account_id / connectedAccounts[].id) against your user. These are the IDs you pass in accounts[]when publishing. You can re-list a user's accounts any time, filtered by your tenant identifier:

curl
curl "https://api.postfuze.com/api/v1/social-accounts?tenant_id=user_42" \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "data": [
    {
      "id": "a1b2c3d4-…",
      "platform": "facebook",
      "tenant_id": "user_42",
      "social_network_id": "f1e2d3c4-…",
      "network_unique_id": "1784523…",
      "username": "acme",
      "display_name": "Acme Co",
      "profile_image_url": "https://…",
      "account_type": "page",
      "status": "active",
      "scopes": ["pages_manage_posts", "pages_read_engagement"],
      "token_expires_at": "2026-08-08T10:00:00.000Z",
      "last_synced_at": "2026-06-09T10:00:00.000Z",
      "created_at": "2026-06-09T10:00:00.000Z",
      "updated_at": "2026-06-09T10:00:00.000Z"
    }
  ]
}

Step 3 — Create posts

Target the stored account IDs. A post fans out to one independent target per account. Use an Idempotency-Key header (or an external_reffield) so retries don't create duplicates — replaying a seen key returns the original post with 200. Per-platform overrides go in config, keyed by platform.

curl
curl https://api.postfuze.com/api/v1/posts \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: campaign_91:launch" \
  -d '{
    "containers": [
      { "content": "Big news today 🚀", "media": [{ "id": "media_abc", "alt_text": "Launch banner" }] },
      { "content": "More details in the thread 👇" }
    ],
    "accounts": ["a1b2c3d4-…", "b2c3d4e5-…"],
    "scheduled_at": "2026-06-10T15:00:00.000Z",
    "config": { "youtube": { "isShort": true } }
  }'

Container 0 is the main post (role: "main"), container 1 becomes the first_comment, and any further containers are thread replies. See First comments & threads and Posts for details.

Step 4 — Stay in sync

Webhooks (preferred)

Register an endpoint and subscribe to events instead of polling. PostFuze returns a signing_secret once, on creation — store it and verify the signature on every delivery.

curl
curl https://api.postfuze.com/api/v1/webhooks \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/hooks/postfuze",
    "events": ["post.published", "post.error", "account.token_expired", "import.completed", "import.failed"],
    "description": "Production sync"
  }'
201 Created
{
  "id": "wh_…",
  "url": "https://yourapp.com/hooks/postfuze",
  "events": ["post.published", "post.error", "account.token_expired", "import.completed", "import.failed"],
  "is_active": true,
  "description": "Production sync",
  "signing_secret": "whsec_…",
  "created_at": "2026-06-09T10:10:00.000Z",
  "updated_at": "2026-06-09T10:10:00.000Z"
}

On a post.published or post.error event, update the matching row in your database. Treat account.token_expired as a signal to prompt the user to reconnect. See Webhooks for the full event catalog, payload shapes and signature verification.

Polling (fallback)

If you cannot receive webhooks, poll GET /posts/{id} until every target leaves the queued / pending state. The post-level status rolls up the per-target statuses, and each target carries its own platform_post_id, platform_post_url and any error_code / error_message. Back off exponentially and stop once all targets are terminal (published or failed).

Suggested database schema

Mirror just enough of PostFuze's state to drive your UI and joins; PostFuze remains the source of truth. A minimal Postgres schema:

schema.sql
-- One row per platform OAuth app you registered (BYOK). Mirrors social_networks.
create table postfuze_networks (
  id              uuid primary key,            -- PostFuze social_networks.id
  platform        text not null,               -- 'x' | 'linkedin' | 'instagram' | …
  display_name    text,
  created_at      timestamptz not null default now(),
  unique (platform)
);

-- One row per connected account, linked to YOUR user.
create table postfuze_accounts (
  id                  uuid primary key,        -- PostFuze social_accounts.id (use in accounts[])
  user_id             uuid not null references users (id),
  tenant_id           text not null,           -- the tenant_id you sent to /auth-url
  platform            text not null,
  network_unique_id   text not null,           -- platform's own id for the account/page
  username            text,
  display_name        text,
  account_type        text,                    -- 'account' | 'page' | …
  status              text not null,           -- 'active' | 'disabled' | …
  token_expires_at    timestamptz,
  last_synced_at      timestamptz,
  created_at          timestamptz not null default now(),
  unique (platform, network_unique_id)
);
create index on postfuze_accounts (user_id);

-- One row per post you create, with a rolled-up status updated from webhooks.
create table postfuze_posts (
  id              uuid primary key,            -- PostFuze posts.id
  user_id         uuid not null references users (id),
  external_ref    text,                        -- your idempotency key
  status          text not null,               -- 'draft'|'scheduled'|'queued'|'published'|'failed'|'canceled'
  scheduled_at    timestamptz,
  published_at    timestamptz,
  created_at      timestamptz not null default now(),
  unique (external_ref)
);

-- One row per (post, account) targetthe unit that succeeds or fails.
create table postfuze_post_targets (
  id                uuid primary key,          -- PostFuze post_targets.id
  post_id           uuid not null references postfuze_posts (id),
  account_id        uuid not null references postfuze_accounts (id),
  platform          text not null,
  status            text not null,             -- 'queued'|'pending'|'published'|'failed'
  platform_post_id  text,
  platform_post_url text,
  error_code        text,
  error_message     text,
  published_at      timestamptz
);
create index on postfuze_post_targets (post_id);

Use your external_ref (the idempotency key) as the natural join between your domain and PostFuze's posts, and network_unique_id when reconciling accounts after a reconnect. Keep postfuze_post_targets.status authoritative from webhook events; the post-level status is a convenience rollup.

Operational notes

  • One key, server-side. Hold a single org-level sk_live_… key on your backend. Do not ship keys to clients. See Authentication.
  • Idempotency everywhere. Always send an Idempotency-Key on POST /posts so retried requests are safe.
  • Handle token expiry. When you receive account.token_expired, mark the account stale and re-run the connect flow for that user.
  • Backfill on connect. To pull a user's existing posts, queue an import with POST /social-accounts/{id}/imports and react to the import.completed webhook. See Imports.

Next steps

  • Quickstart — the five-minute end-to-end walkthrough.
  • Posts — full request reference, scheduling and per-platform config.
  • Webhooks — event catalog, payloads and signature verification.
  • Social accounts — list, inspect, metrics and health.