Skip to main content

OAuth Delegation

OAuth delegation lets a third-party app connect a user’s social media account through PostSyncer and receive back a signed proof of ownership for that account. It works for any platform PostSyncer supports that uses OAuth (TikTok, Instagram, X/Twitter, LinkedIn, YouTube, Facebook, Pinterest, Threads). Instead of integrating each platform’s OAuth yourself, you redirect the user to PostSyncer, PostSyncer runs the platform OAuth, and then redirects the user back to you with the platform’s permanent user id, the handle, and an HMAC-SHA256 signature you can verify with your signing secret.
Delegation does not store a social account on your PostSyncer workspace. The platform_id and handle are read directly from the platform’s OAuth response and passed through to you - nothing else is persisted.
Your API key is a full-access secret and must never appear in the browser. You start delegation with a server-to-server call from your backend (the API key goes in the Authorization header), and PostSyncer returns an opaque, single-use authorize_url. Only that opaque URL is ever sent to the user’s browser.

Prerequisites

  1. A PostSyncer API key (create one).
  2. A signing secret for that API key. In Settings → API Keys, click the shield icon on the API key row and choose Generate signing secret. Copy it - it’s shown only once. Regenerating invalidates the previous secret.
Keep the signing secret private. It is the shared secret used to sign and verify ownership proofs. Anyone with it can forge proofs.

Flow overview

Your server ─(1) POST /oauth/delegate/sessions (api_key in header)─▶ PostSyncer
Your server ◀──────────── { authorize_url } (opaque, single-use) ───

   (2) redirect the user's browser to authorize_url

                               PostSyncer runs platform OAuth (TikTok, X, …)

Your app ◀─(3) redirect────── PostSyncer  (callback_url + signed params)

   (4) verify state, expires, sig  ──▶ trust platform_id as ownership proof

Step 1 - Create a delegation session (server-to-server)

From your backend, call:
POST https://postsyncer.com/api/oauth/delegate/sessions
with your API key in the Authorization header:
Authorization
string
required
Bearer <your PostSyncer API key>. Sent server-to-server only - never exposed to the browser.
platform
string
required
The platform to connect, e.g. tiktok, instagram, twitter, linkedin, youtube, facebook, pinterest, threads.
callback_url
string
required
Absolute URL PostSyncer redirects the user back to after authentication.
state
string
required
A random, single-use string you generate. Used for CSRF protection; PostSyncer echoes it back unchanged.
The response contains an opaque, single-use authorize URL (valid for 15 minutes):
{
  "authorize_url": "https://app.postsyncer.com/oauth/delegate?request=psd_xxx",
  "expires_in": 900
}
Node.js
import crypto from 'crypto';

const state = crypto.randomBytes(16).toString('hex');
// persist `state` server-side (single use) so you can verify it on return

const res = await fetch('https://postsyncer.com/api/oauth/delegate/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.POSTSYNCER_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    platform: 'tiktok',
    callback_url: 'https://yourapp.com/postsyncer/callback',
    state,
  }),
});

const { authorize_url } = await res.json();

Step 2 - Redirect the user to the authorize URL

Send the user’s browser to the authorize_url you received. It carries only an opaque request token - no API key, platform or callback URL.
Node.js
res.redirect(authorize_url);
PostSyncer then runs the platform’s normal OAuth consent screen.

Step 3 - PostSyncer redirects back to your callback_url

On success, PostSyncer redirects the user to your callback_url with:
platform
string
The platform that was connected.
platform_id
string
The platform’s permanent unique identifier for the user (e.g. TikTok open_id, X user_id, Instagram user_id). This is the value you trust as ownership proof.
handle
string
The username on that platform. For display only - handles can change.
state
string
The same value you sent in step 1.
expires
integer
Unix timestamp, 5 minutes from when the proof was issued.
sig
string
HMAC-SHA256 signature of the string platform={platform}&platform_id={platform_id}&handle={handle}&state={state}&expires={expires} using your signing secret.
Example callback
https://yourapp.com/postsyncer/callback
  ?platform=tiktok
  &platform_id=_000abc123
  &handle=janedoe
  &state=9f2b...c4
  &expires=1717000000
  &sig=4d6c...e1
If something fails (or the user cancels), PostSyncer redirects to your callback_url with error, error_description, and state instead.

Step 4 - Verify the proof

On your server, before trusting platform_id:
  1. Verify state matches the value you generated and mark it as used (single use).
  2. Verify sig by recomputing the HMAC over the exact base string.
  3. Verify expires is in the future.
Only if all three pass should you treat platform_id as verified ownership.
import crypto from 'crypto';

function verifyDelegation(query, signingSecret, expectedState) {
  const { platform, platform_id, handle, state, expires, sig } = query;

  // 1. CSRF - must match what you issued, and be single-use
  if (!state || state !== expectedState) return false;

  // 3. Freshness
  if (!expires || Number(expires) < Math.floor(Date.now() / 1000)) return false;

  // 2. Signature
  const base =
    `platform=${platform}&platform_id=${platform_id}` +
    `&handle=${handle}&state=${state}&expires=${expires}`;
  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(base)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Errors

Step 1 (session creation) returns a JSON error to your server with an HTTP 4xx status and a code:
missing_api_key
error
No API key was provided in the Authorization header. (401)
invalid_api_key
error
The API key is invalid or expired. (401)
no_signing_secret
error
The API key has no signing secret. Generate one under Settings → API Keys. (422)
unsupported_platform
error
The platform value is not a supported OAuth platform. (422)
Step 3 (after the user authorizes) errors are redirected back to your callback_url with an error, error_description and state instead of a signed proof:
access_denied
error
The user cancelled or declined the platform consent screen.
connection_failed
error
The platform OAuth did not complete.
expired_request
error
The authorize URL was already used or expired before the user opened it.
The signature only covers successful proofs. Always treat any response that carries an error parameter as a failed connection.