Engineering

How Outstand.so works for building a social media scheduler

Create a developer-focused page that explains the end-to-end flow for scheduling and publishing posts using Outstand.so (authentication, scheduling flow, delivery, retries).

Building a social media scheduler from scratch used to mean weeks of work: separate OAuth flows for X, LinkedIn, Instagram, TikTok, Facebook, and more, each with its own token management, rate limiting quirks, and media format requirements. A developer could spend six months just getting ten platforms working before writing a single line of their actual product.

Outstand.so is a unified social media API that removes all of that. One integration, one data model, one place to handle rate limits and retries, and your scheduler is posting to 12 platforms. Outstand's homepage publishes 12.8M+ posts per month, 500+ companies, 99.92% uptime and a 180ms average API response time.

This page documents the end-to-end flow: from authentication to scheduling to delivery and failure handling.

What Outstand.so does for a scheduler

At its core, Outstand exposes a REST API at https://api.outstand.so/v1. Your scheduler calls it, and Outstand handles everything downstream:

  • Maintaining OAuth tokens for each connected platform
  • Translating your post payload into each platform's native format
  • Queuing posts for future delivery against a UTC timestamp you supply
  • Retrying failed deliveries with exponential backoff
  • Notifying your backend the moment a post publishes or errors via webhooks

The architecture is: your scheduler → POST /v1/posts → Outstand's platform adapters → X, LinkedIn, Instagram, TikTok, Facebook, Threads, Bluesky, YouTube, Pinterest, Google Business Profile, Vimeo, Reddit.

You never touch any of those platform APIs directly. Outstand also provides post lifecycle tracking so you can poll or subscribe to status changes at every step.

Authentication setup

Account creation is free and doesn't require a credit card. After signing up at outstand.so/app/signup, you're redirected to the dashboard where you generate an API key.

All requests authenticate via the Authorization header:

curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"

You can issue multiple API keys per organization, useful for separating staging from production environments or scoping keys to specific services. Full details are in the authentication docs.

For platform credentials, Outstand supports a Bring Your Own Key (BYOK) model. If you're building a white-label product or agency tool, you can supply your own OAuth app credentials for X, LinkedIn, Meta, TikTok, and others so your users see your brand on the OAuth consent screen, not Outstand's. This is configured via:

POST /v1/social-networks
{ "network": "linkedin", "clientKey": "...", "clientSecret": "..." }

For most scheduler builders, though, Outstand's shared credentials are sufficient for getting started immediately. See the backend integration guide for production-level setup patterns, including how to store socialNetworkId, socialAccountId, and postId in your own database.

Core scheduling workflow

Step 1: List connected social accounts

Before you can post, you need the account IDs your users have connected. This is a simple GET call:

curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"

Store the returned id and username values in your database. Posts target accounts through the accounts array, which takes a network name or a username rather than an account id. The list connected social accounts endpoint includes account health status, worth checking before scheduling a batch to avoid queuing posts for expired tokens.

Step 2: Upload media (if applicable)

If your scheduler handles image or video content, upload media before creating the post:

curl -X POST https://api.outstand.so/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "image.jpg" }'

That returns a media id and an upload_url that is valid for one hour, as documented under upload media. PUT the file to that URL, then call POST /v1/media/{id}/confirm to finalise it and get the public URL back. Attach media to a post as a { url, filename } object inside a container: media is referenced by URL, not by id. Outstand downloads the file server-side at publish time, so the URL has to stay publicly reachable over HTTPS and must not require authentication. Outstand does not resize or transcode media, so check each platform's format requirements before you upload.

Step 3: Create a scheduled post

This is the central call in any scheduler. Pass scheduledAt as an ISO 8601 UTC timestamp:

curl -X POST https://api.outstand.so/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"containers": [
{ "content": "Scheduled post from my app" }
],
"accounts": ["x", "linkedin"],
"scheduledAt": "2026-07-01T09:00:00Z"
}'

Omit scheduledAt to publish immediately, and note that a scheduledAt more than 30 days ahead is rejected with a 400. To build a longer-horizon calendar, keep the schedule in your own store and enqueue each post with Outstand as its send time comes within the 30-day window. The API returns a postId immediately, store this for status lookups. You can cancel any scheduled post before it publishes by calling DELETE /v1/posts/{id}. The create a post endpoint attaches media through the media array on a container, by URL. There is no mediaIds parameter: that key is ignored and reported back in warnings.

Step 4: Schedule a first comment

For platforms that support it (LinkedIn, Instagram, YouTube), you can schedule a first comment alongside the post. This is useful for adding a CTA or hashtag block without cluttering the main post body. Use the create a first comment endpoint with a matching postId and its own scheduledAt.

Rate limiting and retry behavior

Every major social platform enforces rate limits, and they differ significantly. X has per-15-minute write windows; Meta's Graph API has hourly and daily post caps; LinkedIn throttles by token scope. Managing this yourself across 12 platforms is a common source of production incidents.

Outstand absorbs most of that for you, but not all of it, and the difference matters when you are writing retry code. Downstream platform rate limits do not surface on the publish call, because the send happens off the request path. Outstand's own API does return 429 Too Many Requests when you exceed your API key's rate limit.

Specifically:

  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Pace your requests off the remaining count and you should not hit the limit.
  • If you do hit it, you get a 429. Honour the Retry-After header when it is present and back off exponentially when it is not. A 429 is retryable and does not mean your key is wrong.
  • Transient transport failures inside the publishing queue are retried automatically with exponential backoff, with no client involvement.
  • Terminal platform failures, such as an expired token or rejected media, are recorded as failed on that account and are not retried into a duplicate post. To republish, delete the failed post and create a new one.
  • If every account in the fan-out fails, the post.error webhook fires. If at least one succeeds, you get post.published instead, with every account enumerated so a partial outcome is visible in one notification.

You can also set usage limits per organization to cap spending in development or sandbox environments.

Webhook events for real-time scheduler feedback

Polling the status endpoint works, but webhooks are cleaner for most scheduler architectures. Outstand sends signed HTTP POST requests to your endpoint for five event types:

Event

When it fires

post.published

At least one target account published successfully

post.error

All target accounts failed after retries

account.token_expired

OAuth token refresh failed; user needs to re-authenticate

import.completed

A post import job finished successfully

import.failed

A post import job finished with errors

Configure your endpoint in Settings → Webhooks. Add a signing secret and Outstand includes an X-Outstand-Signature header (HMAC-SHA256 of the raw body) so you can verify the payload wasn't tampered with.

A minimal Node.js webhook handler:

const crypto = require('crypto');

app.post('/webhooks/outstand', (req, res) => {
// Verify signature
const sig = req.headers['x-outstand-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(JSON.stringify(req.body), 'utf8')
.digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}

// Acknowledge fast, process async
res.status(200).send('OK');

const { event, data } = req.body;
if (event === 'post.published') {
// Mark post as published in your DB using data.postId
} else if (event === 'post.error') {
// Surface error to user, log for retry review
} else if (event === 'account.token_expired') {
// Prompt user to reconnect the account
}
});

Full event schemas and retry behavior are documented in the webhooks reference. Outstand retries unacknowledged webhook deliveries up to 5 times with exponential backoff, so your endpoint should respond with a 2xx within 30 seconds.

Full scheduler job example (TypeScript)

Here's a complete scheduler job that pulls pending posts from your queue and publishes them via Outstand:

const API_KEY = process.env.OUTSTAND_API_KEY!;
const BASE_URL = 'https://api.outstand.so';

async function runSchedulerJob(scheduledPost: {
content: string;
accounts: string[];
scheduledAt: string; // ISO 8601 UTC
idempotencyKey: string;
}) {
const res = await fetch(`${BASE_URL}/v1/posts`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
// Prevent double-posting on retries
'Idempotency-Key': scheduledPost.idempotencyKey,
},
body: JSON.stringify({
containers: [{ content: scheduledPost.content }],
accounts: scheduledPost.accounts,
scheduledAt: scheduledPost.scheduledAt,
}),
});

const { data: post } = await res.json();
console.log(`Post queued: ${post.id}, scheduled for ${scheduledPost.scheduledAt}`);
return post.id;
}

Note the Idempotency-Key header. If your scheduler retries the job due to a network timeout, the same key prevents Outstand from creating a duplicate post. Use a stable identifier (e.g., a UUID generated when the user creates the schedule entry in your DB) rather than generating a new one per attempt.

For the Python equivalent, see the getting started guide.

Supported platforms and constraints

Outstand currently supports 12 platforms: X, LinkedIn, Instagram, TikTok, Facebook, Threads, Bluesky, YouTube, Pinterest, Google Business Profile, Vimeo, and Reddit.

A few platform-specific constraints to know:

  • Instagram: Managed Keys are included, so you do not need to register your own Meta app. Supports feed posts, Reels, and carousels. See Instagram configuration.
  • X/Twitter: BYOK required, so you register your own X app and add its credentials before connecting an account. X's OAuth refresh tokens are single-use and rotate on every refresh, and X can invalidate them well before their documented lifetime with no stated reason, so an account can need reconnecting even when your integration is correct. Reconnecting is currently the only reliable fix. See X token refresh known issues.
  • TikTok: Managed Keys are included, so no TikTok app registration is required. Direct post publishing, not just draft creation, requires approved API access on TikTok's side.
  • Character limits and media specs: These vary by platform. The only limit Outstand enforces at request time is the Threads 500-character cap per container, which is rejected with a 400 on create and update. Every other character limit and all media specs are validated by the platform itself at publish time, so a caption or file a platform rejects fails after the post is queued rather than returning an API error.

Check the account health endpoint before bulk scheduling to catch token expiry and permission issues proactively.

How do you read analytics across platforms after a post publishes?

Once a post is published, call GET /v1/posts/{id}/analytics to read engagement for every account it went out to in one request. The response carries a metrics_by_account array and an aggregated_metrics object holding total likes, comments, shares, views, impressions, reach and an average engagement rate. The post has to be published already: a draft or a scheduled post returns 400. The full field list is in the get post analytics reference.

The response shape is the same whichever platforms the post targeted. The numbers inside it are not, and that is the part to design for:

  • An account can come back with metrics set to null. When it does, metrics_error.code says why: token_expired, scope_missing, post_not_found, metrics_expired or unknown.
  • Individual fields are nullable even when metrics are present. Likes, comments, impressions, reach and engagement rate are all typed as number or null.
  • Reddit has no insights API. You get score, upvote ratio and comment count, and no impressions or reach at all.
  • On Facebook, engagement fields Meta does not expose for a given node type come back as 0 and are named in platform_specific.unavailable_fields. Check that array before you read a zero as real, because the aggregated total for shares undercounts while it is non-empty.
  • Instagram Stories use a different metric set. Likes, comments and saves are null, while replies, profile visits, follows, link clicks, reposts, total interactions and a navigation breakdown are populated instead. Story insights are only queryable for roughly 24 hours after publish, after which that account returns null metrics with metrics_expired.

Read metrics_by_account rather than the aggregate if you care whether a number is real. aggregated_metrics always emits all seven totals as numbers, whatever the networks involved supported and whatever went wrong. A post to a network with no impressions surface aggregates to total_impressions: 0, and an account whose metrics failed outright still contributes zeros to the totals. So a post you failed to measure and a post nobody engaged with are the same seven zeros in the aggregate. The per-account array is where the difference shows up, as a null metrics object with a metrics_error beside it. Verified against the implementation on 2026-08-30.

For account-level metrics, follower counts and per-network engagement, use GET /v1/social-accounts/{id}/metrics with optional since and until Unix timestamps, which default to the last 30 days. Coverage varies by network. TikTok, YouTube and Bluesky are current-snapshot only and ignore the date range, and Bluesky exposes no engagement metrics at all.

One cost to know before you put that endpoint behind a dashboard refresh. On X, account metrics are aggregated by reading up to 500 of the account's own posts inside the date range, and those reads count against your own connected X app quota and pay-per-use cost. Our docs put it at roughly $0.005 per post read, and a busy account at up to about $2.50 per call. Cache the result rather than polling it. That is the honest limit of the "one integration" pitch: the publishing integration is ours, but on X the metering is still yours.

FAQ: common pitfalls

Timezones. scheduledAt must be UTC (e.g., 2026-07-01T09:00:00Z). If your users input local times, convert to UTC before calling the API. Passing a naive timestamp without the Z suffix will cause validation errors.

Idempotency. Network timeouts happen. Always pass an Idempotency-Key header on POST /v1/posts. Use a stable, per-schedule-entry UUID stored in your database, not one generated per request. Outstand remembers the key for 24 hours, scoped to your organization and to this endpoint, so a retry after a timeout returns the original response with an Idempotency-Replayed: true header instead of creating a duplicate post.

Webhook duplicates. Outstand retries webhook delivery if your endpoint doesn't return 2xx within 30 seconds. Your handler should check postId against already-processed IDs before doing work. Return 200 immediately, process asynchronously.

Token expiry. The account.token_expired webhook is your signal to prompt users to reconnect an account. There is no per-account reconnect endpoint, so send the user through the full OAuth flow again, starting at /v1/social-networks/{network}/auth-url, then finalize the pending connection after OAuth completes. Reconnecting a handle that is already connected under the same tenantId replaces the existing account rather than creating a second one.

Formatting differences. A post that renders well on LinkedIn (long-form, line breaks) may get truncated on X. The containers array accepts platform-specific content overrides if you want to tailor copy per platform while sending a single API request.

Media upload failures. Media is referenced by URL, not by id. Upload through upload media, PUT the bytes, confirm, then pass the returned public URL as { url, filename } on a container. The URL is fetched at publish time rather than at create time, so a link that expires between the two will fail the post.

Pricing

Outstand charges $5/month which includes 1,000 posts, then $0.01 per post after that. No seat licenses, no per-platform fees, no annual contracts. For a scheduler handling 5,000 posts/month, that's $45. A comparable fixed-tier plan from most alternatives runs $99+/month at the same volume.

For context: building 12 platform integrations yourself typically takes 6+ months of engineering time before reaching the first user. Outstand replaces that with a 5-minute signup and a single API key.

See full pricing details and a live cost calculator at outstand.so.

Ready to build? Start with the getting started guide or read the 7 best APIs for scheduling posts across multiple social platforms for a broader comparison of what's available in 2026.