A customer clicks Connect LinkedIn in your product, authorizes access, chooses a profile or Company Page, and returns to your settings screen. Your application now knows which social accounts belong to that customer's workspace.
That last part matters as much as OAuth. A successful authorization is not a complete onboarding flow until the right accounts are associated with the right tenant, the customer has confirmed their selection, and your UI can distinguish a connected account from an unfinished attempt.
This tutorial implements that boundary with Outstand: generate an authentication URL on your backend, handle the return, finalize page selection, and list accounts for the authenticated tenant. LinkedIn and Facebook provide the concrete multi-account flow.
1. Separate credentials, accounts, and tenants
Three identifiers serve different purposes:
- Network credentials identify the application requesting platform access.
- Social accounts identify the profiles or Pages your customer connects.
- Your tenant ID identifies the customer workspace inside your SaaS.
Your Outstand organization can contain accounts for multiple SaaS customers. The API key authenticates that organization; it does not authenticate your individual customers. Your backend must enforce workspace membership on every connection, listing, publishing, and disconnection action.
Use a stable internal workspace identifier, such as workspace_482, rather than an email address or workspace name. Outstand accepts alphanumeric characters, underscores, and hyphens for tenant identifiers.
There is an important naming difference: send tenant_id when generating an authentication URL, but use tenantId when filtering the account list. Do not accept either value blindly from the browser. Resolve it from the signed-in user's authorized workspace.
2. Choose the credential setup before adding Connect
Managed Keys and BYOK configuration determines which application appears on the provider's authorization screen.
Managed Keys are included with your subscription for LinkedIn, Instagram, Facebook, Threads, YouTube, Pinterest, and TikTok. Bluesky is also covered by the managed setup, but uses an app password instead of this OAuth redirect flow. X, Google Business Profile, Vimeo, and Reddit require your own credentials.
For supported OAuth networks, Managed Keys avoid registering your own developer application. Choose BYOK when you need your own application identity on the authorization screen, and complete the platform's required configuration and review steps. An onboarding screen inside your SaaS does not, by itself, change the provider's consent-screen branding.
Before requesting an authentication URL, ensure the network is configured in your Outstand organization. For the broader product context, see white-label social media management. Here, we will focus on the connection implementation rather than rebuilding a social dashboard.
3. Generate the URL from your backend
The request is:
1POST https://api.outstand.so/v1/social-networks/linkedin/auth-url
2Authorization: Bearer <server-side-api-key>
3Content-Type: application/json
4
5{
6 "tenant_id": "workspace_482",
7 "redirect_uri": "https://app.example.com/integrations/linkedin/return",
8 "force_account_selection": true
9}The response is { "success": true, "data": { "auth_url": "..." } }. Redirect the customer to data.auth_url; generate a fresh URL for every attempt rather than caching it across customers. See the authentication URL reference for request options.
Your redirect_uri is the final destination in your application, not the OAuth callback registered with LinkedIn or Facebook. Outstand handles the provider callback first. Omitting this destination sends the customer back to the Outstand dashboard instead of your onboarding screen.
The following backend TypeScript illustrates initiation and return handling. requireWorkspaceAdmin and flows are your application's adapters, not Outstand SDK methods. The first authenticates the request and checks workspace permissions. The second is durable, expiring server-side storage, scoped by both user and tenant, with atomic state transitions. Inject the API key from your secret store.
1type Network = 'linkedin' | 'facebook';
2type Actor = { userId: string; tenantId: string };
3type Page = {
4 id: string;
5 name?: string;
6 username?: string;
7 profilePictureUrl?: string | null;
8};
9type Pending = {
10 success: true;
11 data: { network: string; expiresAt: number; availablePages: Page[] };
12};
13
14async function outstand<T>(path: string, body?: unknown): Promise<T> {
15 const response = await fetch(`https://api.outstand.so${path}`, {
16 method: body === undefined ? 'GET' : 'POST',
17 headers: {
18 Authorization: `Bearer ${apiKey}`,
19 'Content-Type': 'application/json',
20 },
21 body: body === undefined ? undefined : JSON.stringify(body),
22 cache: 'no-store',
23 });
24 const result = await response.json() as T & {
25 success: boolean;
26 error?: string;
27 };
28 if (!response.ok || !result.success) {
29 throw new Error(`Connection request failed (${response.status})`);
30 }
31 return result;
32}
33
34async function start(request: Request, network: Network) {
35 const actor: Actor = await requireWorkspaceAdmin(request);
36 const flow = crypto.randomUUID();
37 await flows.create(flow, { ...actor, network }, 30 * 60);
38 const callback = new URL(
39 `/integrations/${network}/return`,
40 'https://app.example.com',
41 );
42 callback.searchParams.set('flow', flow);
43 const result = await outstand<{
44 success: true;
45 data: { auth_url: string };
46 }>(`/v1/social-networks/${network}/auth-url`, {
47 tenant_id: actor.tenantId,
48 redirect_uri: callback.toString(),
49 force_account_selection: true,
50 });
51 return Response.redirect(result.data.auth_url, 303);
52}
53
54async function receive(request: Request, network: Network) {
55 const actor = await requireWorkspaceAdmin(request);
56 const url = new URL(request.url);
57 const flowId = url.searchParams.get('flow') ?? '';
58 // Requires an unexpired, unused flow owned by this actor and network.
59 await flows.requireStarted(flowId, actor, network);
60 if (url.searchParams.has('error')) {
61 await flows.fail(flowId, actor);
62 return cleanRedirect('/settings/social?connection=failed');
63 }
64 const token = url.searchParams.get('session');
65 if (!token) throw new Error('Missing connection session');
66 const pending = await outstand<Pending>(
67 `/v1/social-accounts/pending/${encodeURIComponent(token)}`,
68 );
69 if (pending.data.network !== network) {
70 throw new Error('Unexpected connection network');
71 }
72 await flows.bindPending(flowId, actor, {
73 token,
74 pages: pending.data.availablePages,
75 expiresAt: pending.data.expiresAt,
76 });
77 return cleanRedirect('/settings/social/select');
78}Implement cleanRedirect as a 303 response with Cache-Control: no-store and Referrer-Policy: no-referrer. The selection screen retrieves its pending attempt through the authenticated application session, not a raw Outstand session token in its URL. Configure callback logging to redact query strings, and load no third-party assets before redirecting away from the callback.
Protect the initiation POST with your normal CSRF controls. Validate the route's network against an allowlist at runtime; a TypeScript type alone cannot validate an incoming request. The flow parameter is your correlation identifier, not a replacement for Outstand's OAuth state handling. Keep the destination fixed server-side rather than accepting an arbitrary browser-provided return URL.
4. Treat page selection as a separate step
For LinkedIn and Facebook, the return contains ?session=..., not a finished account ID. Read the pending connection from your backend:
1{
2 "success": true,
3 "data": {
4 "network": "linkedin",
5 "expiresAt": 1794312000000,
6 "availablePages": [
7 { "id": "123456", "name": "Example Company" }
8 ]
9 }
10}expiresAt is a Unix timestamp in milliseconds. Pending connections last 30 minutes. Available entries contain picker metadata, not provider access tokens. LinkedIn can offer a personal profile and organization accounts; Facebook offers Pages. Render labels and images when available, allow explicit selection, and show an actionable empty state if no eligible account appears.
Both pending endpoints require your API key and verify that the session belongs to the same Outstand organization. Invalid, expired, or foreign-organization sessions return 404. That check does not replace tenant authorization inside your SaaS, where several tenants may share one organization.
After selection, finalize the connection from the backend:
1type Connected = {
2 id: string;
3 nickname: string | null;
4 username: string | null;
5 network: string;
6 accountType: string | null;
7};
8
9async function finalize(request: Request) {
10 const actor = await requireWorkspaceAdmin(request);
11 const { selectedPageIds } = await request.json() as {
12 selectedPageIds: unknown;
13 };
14 // Atomically locks this actor's unexpired pending attempt.
15 const attempt = await flows.claimPending(actor);
16 if (!Array.isArray(selectedPageIds) || !selectedPageIds.length ||
17 !selectedPageIds.every((id): id is string =>
18 typeof id === 'string' &&
19 attempt.pages.some((page: Page) => page.id === id))) {
20 await flows.release(attempt.id, actor);
21 throw new Error('Choose an available account');
22 }
23 const result = await outstand<{
24 success: true;
25 connectedAccounts: Connected[];
26 }>(
27 `/v1/social-accounts/pending/${encodeURIComponent(attempt.token)}/finalize`,
28 { selectedPageIds },
29 );
30 await flows.complete(attempt.id, actor, result.connectedAccounts);
31 return result.connectedAccounts;
32}Apply CSRF protection to this POST too. Notice that connectedAccounts is top-level, not inside data. Outstand carries the original tenant_id into the selected accounts; finalization does not take a new tenant identifier. Successful finalization deletes the pending session.
In production, handle failed requests around the claimed attempt. Release a lock after a definite validation failure; after a timeout, reconcile the tenant's account list before offering another attempt. Do not assume an interrupted response means no accounts were created.
5. Refresh your tenant-scoped account list
After finalization, fetch:
1GET https://api.outstand.so/v1/social-accounts?tenantId=workspace_482&limit=50&offset=0
2Authorization: Bearer <server-side-api-key>The account list returns success, data, count, total, limit, and offset. Use total to paginate, and retain the returned account IDs as server-verified references for later operations. Accounts connected without a tenant identifier do not match this tenant filter.
Always supply the tenant resolved from your authenticated workspace. Before accepting an account ID for publishing or deletion, verify that it belongs to that tenant. A browser-supplied account ID is not authorization.
Do not force every platform through the LinkedIn picker. For example, Instagram's direct-completion callback can include success=true, account_id, network_unique_id, and username, or an error. Treat these as navigation hints, then verify the account through your backend. Use each network's configuration guide rather than assuming identical callback parameters.
6. Make reconnect and disconnect part of onboarding
Reconnect should restart authorization with the same tenant identifier. Outstand replaces an existing account for the same organization, network, and tenant when its platform identity or normalized username matches, refreshing credentials instead of creating a duplicate. The same platform account connected under another tenant is a separate record.
force_account_selection: true helps customers connect a second account instead of silently reusing their current browser login. It is honored by TikTok, Facebook, Instagram, YouTube, and Google Business Profile, and ignored where the network exposes no equivalent. Do not promise a chooser on every platform.
Your API key needs accounts:connect; assigning a tenant also requires accounts:assign_tenant, and replacing an account requires accounts:reconnect. Check organization permissions when a previously working flow starts returning 403.
Offer distinct UI states for awaiting authorization, awaiting selection, connected, failed, and reconnect required. Cancellation and expired sessions should return customers to a useful settings screen with a fresh Connect action, not leave a permanent loading indicator.
Finally, put a confirmation before disconnecting an account. Deletion is irreversible, removes the stored connection, and requires a new authorization flow to reconnect. It does not delete posts already published on the social platform.
Before shipping, walk through these cases with separate test workspaces:
- Start a connection in one workspace, switch workspaces, and confirm the callback cannot finalize under the second workspace.
- Cancel at the provider, return after the pending session expires, and double-submit the selection form.
- Select multiple Pages and verify that only the chosen Pages appear in the tenant-scoped list.
- Reconnect an existing account and confirm your UI does not display a duplicate.
- Attempt to disconnect an account ID belonging to another workspace and verify your backend rejects it before calling Outstand.
The implementation is complete when the customer can connect, select, return, and reconnect without your backend ever confusing one workspace's accounts with another's. The Connect button is only the entry point; tenant-safe completion is the feature.