Connect API — Build Your Own Connect Page
The Connect API lets you build the "connect your social accounts" experience inside your own product, on your own domain, with your own design — instead of sending end users to the hosted access_url page described in the White-label Integration Guide.
Your page requests a platform authorize URL from Upload-Post, redirects the end user to the social network's consent screen, and Upload-Post handles the OAuth exchange and token storage. Whether the connection succeeds, fails, or the user cancels, the user is sent back to the URL you choose with a machine-readable outcome.
When to use which:
Hosted page (access_url) | Connect API (this page) | |
|---|---|---|
| Setup effort | None — one API call | You build the UI |
| Branding | Logo, title, colors, language | 100% yours — it's your page |
| Domain the user sees | app.upload-post.com | Yours (except the OAuth hops) |
Note: During the OAuth flow the user always visits the social network's own consent screen, and the network redirects briefly through
app.upload-post.com(the OAuth callback registered with each platform) before returning to yourredirect_url. This hop lasts milliseconds and is required by the platforms' OAuth policies — it applies to every provider in the industry.
How it works
Your backend Your connect page Upload-Post Social network
│ │ │ │
│ 1. generate-jwt (API key) │ │ │
│───────────────────────────>│ │ │
│ profile JWT │ │ │
│ │ 2. POST /oauth/<platform>/start (profile JWT) │
│ │───────────────────────────>│ │
│ │ authorize_url + state │ │
│ │ 3. redirect user ──────────────────────────────────>│
│ │ │ 4. user authorizes │
│ │ │<── code + state ───────│
│ │ │ 5. token exchange, │
│ │ │ account stored │
│ │<── 6. redirect to your redirect_url ────────────────│
│ │ ?connect_status=success|error|cancelled │
- Your backend calls
generate-jwtwith your API key to obtain a profile token for the end user's profile. - Your connect page calls the start endpoint below with that profile token and receives the
authorize_url. - You redirect the user to
authorize_url(the social network's consent screen). - After consent, the network redirects to Upload-Post's registered callback, which completes the exchange — authenticated by the single-use
state, so the user's browser needs no session with Upload-Post. - The user lands back on your
redirect_urlwith?connect_status=success&platform=<platform>— or, when they cancelled or something failed, withconnect_status=cancelled/connect_status=errorand a stableerror_code(see Handling the return).
Start endpoint
POST /api/uploadposts/oauth/{platform}/start
Supported platforms: tiktok, instagram, facebook, linkedin, youtube, x (alias: twitter), threads, pinterest, google-business, snapchat
Reddit OAuth (reddit) currently returns HTTP 503 error_code: "reddit_unavailable".
Authentication
Any of:
- Profile JWT (recommended for browser calls):
Authorization: Bearer PROFILE_JWT— the token fromgenerate-jwt. The profile is taken from the token. - API key (server-to-server):
Authorization: Apikey YOUR_API_KEY— pass the profile in the body.
Never expose your API key in a browser; use the profile JWT there.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
profile | string | Only with API key auth | Profile username the connected account will be linked to. Ignored when a profile JWT is used. |
redirect_url | string | No | Absolute http(s) URL (max 2000 chars) to send the end user back to once the attempt is over — on success, on failure and on cancel. connect_status, platform and (on failure) error_code are appended as query parameters. |
Response
{
"success": true,
"platform": "instagram",
"authorize_url": "https://www.instagram.com/oauth/authorize?client_id=...&state=...",
"state": "e34zUEyZjWy6EFeI7IzGSEiUfY8O4K4_",
"expires_in": 900
}
Redirect the end user to authorize_url within 15 minutes (expires_in) — the underlying state is single-use and expires after that window. Mint a fresh one per connection attempt.
Example — browser (profile JWT)
const resp = await fetch(
'https://api.upload-post.com/api/uploadposts/oauth/instagram/start',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${profileJwt}`,
},
body: JSON.stringify({
redirect_url: 'https://yourapp.com/social/connected',
}),
}
);
const { authorize_url } = await resp.json();
window.location.href = authorize_url;
Example — server (API key)
curl -X POST "https://api.upload-post.com/api/uploadposts/oauth/tiktok/start" \
-H "Authorization: Apikey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profile": "end-user-123",
"redirect_url": "https://yourapp.com/social/connected"
}'
Errors
| Status | Meaning |
|---|---|
400 | Missing profile (API key auth) or invalid redirect_url |
401 | Missing or invalid authentication |
404 | Unknown platform, or profile not found for this account (error_code: PROFILE_NOT_FOUND) |
500 | Platform client not configured on the server |
Handling the return
Every attempt ends on your redirect_url (when you passed one) with the same redirect_url for all outcomes — there is no separate failure URL to configure. Upload-Post appends these query parameters:
| Parameter | Values | Description |
|---|---|---|
connect_status | success, cancelled, error | Outcome of the attempt. |
platform | the platform you started | Same public names as the start endpoint (tiktok, instagram, x, …). |
error_code | see below | Only on cancelled / error. Stable and machine-readable — key your UI on this, never on the description. |
error_description | free text, ≤ 200 chars | Only when the social network returned one. Not guaranteed, not localized; safe to display but not to parse. |
Success:
https://yourapp.com/social/connected?connect_status=success&platform=instagram
To confirm the connection server-side (recommended), call GET /api/uploadposts/users and check the profile's social_accounts.
The user cancelled on the consent screen:
https://yourapp.com/social/connected?connect_status=cancelled&platform=facebook&error_code=ACCESS_DENIED
The connection failed:
https://yourapp.com/social/connected?connect_status=error&platform=linkedin&error_code=CONNECTION_FAILED
Error codes
error_code | connect_status | Meaning | Suggested action |
|---|---|---|---|
ACCESS_DENIED | cancelled | The end user declined or closed the network's consent screen. | Offer a retry. |
PROVIDER_ERROR | error | The social network returned an OAuth error other than a cancel (outage, app misconfiguration on their side). error_description usually carries their message. | Offer a retry; escalate if it persists. |
ACCOUNT_ALREADY_LINKED | error | That social account is already connected to a different Upload-Post user. Each social account can be linked to one user at a time. | Tell the user to connect a different account, or contact support to move it. |
INVALID_STATE | error | The state was missing, expired (15 min), already used, or did not match. | Mint a fresh start and retry. |
CONNECTION_FAILED | error | The token exchange or profile fetch failed on the Upload-Post side. | Offer a retry; contact support if it persists. |
Each attempt maps to exactly one return: the state minted at start is single-use, so a retry always goes through a fresh POST /oauth/{platform}/start.
Without a redirect_url, a failed or cancelled attempt shows the Upload-Post error screen with a human-readable message and the user can retry from your page with a freshly minted start.
Example — reading the outcome on your page
const params = new URLSearchParams(window.location.search);
const status = params.get('connect_status'); // success | cancelled | error
const platform = params.get('platform');
if (status === 'success') {
showConnected(platform);
} else if (params.get('error_code') === 'ACCESS_DENIED') {
showRetry(platform, 'You closed the authorization window.');
} else {
showRetry(platform, `Could not connect ${platform} (${params.get('error_code')}).`);
}
Security model
- The
statein the authorize URL is a 192-bit random value stored server-side, single-use and valid for 15 minutes. It is what authenticates the OAuth callback, so the end user's browser never needs an Upload-Post session. - The
stateis bound to the platform, profile and account that minted it — it cannot be replayed, reused across platforms, or combined with another account's session. - OAuth secrets (PKCE verifiers for TikTok and X) never leave the server; only the S256 challenge appears in the authorize URL.
redirect_urlis validated to be an absolutehttp(s)URL, is never followed server-side, and is only ever reached by the end user's own browser — after a successful connection, or after a failed or cancelled one together withconnect_statusanderror_code.
Notes per platform
youtube: profiles configured with custom YouTube credentials authorize against their own Google client automatically.x/twitter: both names are accepted; responses always reportplatform: "x".snapchat: connection is limited to basic profile scopes (posting requires Snapchat Public Profile API approval).instagram: the end user's Instagram must be a Business or Creator account, same as the hosted flow.