Connecting Accounts
Before you can post, your users need to connect their social media accounts. This guide walks through every connection method — OAuth, App Password, and Bot Token — including the entity selection step required by some platforms.
Prerequisites: You need an API key with accounts permission. Connected accounts are grouped under a brand; pass brandId to target a specific brand, or omit it to use your default brand. See Authentication and the Accounts API.
Connected account limits: Each plan includes a set number of connected accounts (Free: 2, Personal: 10, Team: 25, Agency: 100). A connected account is one profile on one platform. Connecting a new account when you're at the limit returns an account_limit_reached error. Upgrade your plan to connect more. See Rate Limits for details.
Connection methods
| Method | Platforms | Flow |
|---|---|---|
| OAuth 2.0 | Twitter, Instagram, Facebook, Threads, LinkedIn, TikTok, YouTube, Pinterest, Google Business Profile | GET redirect URL → user authorizes → callback |
| App Password | Bluesky | POST credentials directly |
| Bot Token | Telegram | POST bot token → register channel(s) |
| Dashboard only | Mastodon | Connect on the Schedulala dashboard (per-instance OAuth), then post via the API |
Mastodon is the one exception: its OAuth is per-instance, so the API connect endpoint does not support it yet. Connect the account once on the Schedulala dashboard and it becomes available to the API immediately (posting, accounts, analytics).
OAuth flow (9 platforms)
Twitter, Instagram, Facebook, Threads, LinkedIn, TikTok, YouTube, Pinterest, and Google Business Profile all use the same redirect-based OAuth flow. For Google Business Profile, the first business location on the Google account is connected automatically.
Step 1: Get the OAuth URL
Request an authorization URL for the platform. Both query parameters are optional: brandId defaults to your default brand, and redirectUrl (where the user is sent after authorizing) defaults to Schedulala's hosted completion page.
curl "https://schedulala.com/api/v1/connect/instagram?brandId=brand_abc123&redirectUrl=https://yourapp.com/callback" \-H "Authorization: Bearer sk_live_abc123xyz789..."
Step 2: Redirect the user
Open the authUrl in the user's browser (or a popup/webview). The user will authenticate with the platform and grant your app access. After authorizing, the platform redirects back to your redirectUrl with the connection result.
Important: The redirectUrl is where your user lands after the platform authorization completes. If you omit it, the user lands on Schedulala's hosted completion page at /developers/oauth-complete.
Step 3: Handle the callback
When the user is redirected back, check the query parameters. A successful connection includes success=true and the account_id. Platforms that need a sub-entity choice instead return success=pending with selectionRequired=true and a selectionToken; see the next section.
// In your callback route (e.g. /callback)const url = new URL(request.url);const success = url.searchParams.get('success');const accountId = url.searchParams.get('account_id');const selectionRequired = url.searchParams.get('selectionRequired');const selectionToken = url.searchParams.get('selectionToken');if (success === 'true') {// Account is fully connected and ready to postconsole.log('Connected account:', accountId);} else if (success === 'pending' && selectionRequired === 'true') {// Need to select an entity (page, org, channel, board)// Use selectionToken with the /entities and /select endpoints below} else {const error = url.searchParams.get('error');const message = url.searchParams.get('message');console.error('Connection failed:', error, message);}
Entity selection
Four platforms require selecting a sub-entity after the initial OAuth connection: Facebook (Pages), LinkedIn (Organizations), YouTube (Channels), and Pinterest (Boards). Until an entity is selected, the account cannot be used for posting. Telegram uses a separate channel registration flow; see the Telegram section below.
List available entities
Pass the selectionToken from the OAuth callback. Selection sessions expire after 10 minutes.
curl "https://schedulala.com/api/v1/connect/facebook/entities?selectionToken=sel_abc123..." \-H "Authorization: Bearer sk_live_abc123xyz789..."
Select an entity
Once the user picks an entity, call the select endpoint with the selectionToken and the entity's id. After this, the account is fully connected and ready for posting.
curl -X POST "https://schedulala.com/api/v1/connect/facebook/select" \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"selectionToken": "sel_abc123...","entityId": "page_123"}'
Bluesky (App Password)
Bluesky doesn't use OAuth. Instead, users provide their handle (sent as identifier) and an app password generated from Bluesky settings. The connection is established in a single POST request.
curl -X POST "https://schedulala.com/api/v1/connect/bluesky" \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"brandId": "brand_abc123","identifier": "yourhandle.bsky.social","appPassword": "xxxx-xxxx-xxxx-xxxx"}'
Security: App passwords should be treated like API keys. Never log them or expose them in client-side code. Encourage your users to create a dedicated app password for your integration.
Telegram (Bot Token)
Telegram uses a Bot Token flow. Your user creates a bot via @BotFather, adds the bot as an administrator to each channel they want to post to, then provides the token. Because Telegram has no API to list a bot's channels, you register each channel explicitly by its chat id or @username.
Create a bot
Message @BotFather on Telegram with /newbot and follow the prompts to get a bot token.
Add bot to the channel
Add the bot as an administrator with permission to post messages to each Telegram channel you want to post to.
Connect and register channels
POST the bot token (optionally with channels to register at the same time), then register more channels later with the channels endpoint.
A bot with no registered channel cannot post. Registration is explicit because Telegram cannot list a bot's channels. When you connect with no channels, the response includes a notice telling you to add one. At post time, Schedulala posts to the channel named by platformSettings.telegram.channelId (a chat id or @username), or the first active registered channel if you don't name one.
Step 1: Connect with the bot token
POST the botToken. You can optionally include a channels array (up to 10 chat ids or @usernames) to register them in the same request. Each channel is validated against the Telegram API, confirming the bot is an administrator that can post.
curl -X POST "https://schedulala.com/api/v1/connect/telegram" \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"brandId": "brand_abc123","botToken": "7123456789:AAH...","channels": ["@yourchannel"]}'
Response when no channels are registered:
{"success": true,"account": {"id": "67abc456def789012345cdef","platform": "telegram","username": "yourbot","displayName": "Your Bot","isConnected": true,"channels": []},"notice": "No channels registered yet. The bot cannot post until you add one via POST /api/v1/connect/telegram/channels."}
Step 2: Register a channel
Register a channel by its numeric chat id or @username. Schedulala validates that the bot is an administrator with permission to post in the channel before saving it. Registering the same channel twice returns a conflict (409). Pass accountId when you have more than one connected Telegram bot.
curl -X POST "https://schedulala.com/api/v1/connect/telegram/channels" \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"channel": "@yourchannel"}'
List registered channels
Retrieve the channels currently registered on a connected bot. Pass accountId to target a specific bot when you have more than one.
curl "https://schedulala.com/api/v1/connect/telegram/channels" \-H "Authorization: Bearer sk_live_abc123xyz789..."
Health checks
Connections can break over time — tokens expire, users revoke access, or passwords change. Use health checks to proactively detect issues before posts fail.
Single account
curl "https://schedulala.com/api/v1/accounts/acc_ig_123/health" \-H "Authorization: Bearer sk_live_abc123xyz789..."
Bulk health check
Check all accounts at once. Useful for a connection status dashboard.
curl "https://schedulala.com/api/v1/accounts/health" \-H "Authorization: Bearer sk_live_abc123xyz789..."
Tip: Run bulk health checks daily (e.g., via cron) and alert your users when a connection is unhealthy. This prevents silent post failures and reduces support tickets.
Disconnecting accounts
Warning: Disconnecting an account clears its stored tokens. Scheduled posts targeting it will fail to publish unless the account is reconnected.
curl -X DELETE "https://schedulala.com/api/v1/accounts/acc_ig_123" \-H "Authorization: Bearer sk_live_abc123xyz789..."
Troubleshooting
Common connection errors and how to resolve them.
| Error | Cause | Solution |
|---|---|---|
oauth_denied | User denied the OAuth prompt on the platform | Retry the OAuth flow and have the user approve access. |
conflict | Account is already connected to this brand | Disconnect the existing account first, then reconnect. |
authentication_failed | Wrong app password (Bluesky) or invalid bot token (Telegram) | Double-check the credentials. For Bluesky, generate a new app password. For Telegram, verify the bot token with @BotFather. |
expired (health status) | Health check reports the OAuth token expired and refresh failed | Reconnect the account by running the OAuth flow again. Schedulala auto-refreshes tokens, but some platforms revoke tokens after long inactivity. |
not_found (selection session) | More than 10 minutes passed between OAuth and entity selection, so the selection session expired | Restart the connection flow, then call the /entities and /select endpoints within 10 minutes. |
token_exchange_failed | The platform rejected the token exchange during the OAuth callback | Check the platform's status page and retry the OAuth flow. |