Getting Started
Get started with Outstand, the unified social media API for connecting accounts, publishing posts, and building a social media scheduler across platforms.
Outstand offers a unified API to access a variety of social media platforms. Our ambition is to provide a simple to use API that covers most of the use cases you might need.
Core Features
Building an Instagram inbox? Start with the Conversations API to read inbound direct messages, send replies, and track delivery. Use the sidebar dropdown to switch between Publishing API and Conversations API.
- Social Accounts: Connect your social media accounts to our API.
- Posts: Create and manage your posts.
- Scheduling: Schedule your posts to be published at a later time.
- First comment scheduling: Schedule your first comment to be published at a later time, as long as it's supported by the social network.
- Media: Attach media to your posts, video or images
Supported Platforms
One integration, twelve networks. Outstand supports the following platforms - use the
canonical value in the left column wherever the API expects a network:
| Platform | network value | Connect via | Credentials |
|---|---|---|---|
| X (Twitter) | x | OAuth | BYOK required |
linkedin | OAuth | Managed Keys | |
instagram | OAuth | Managed Keys | |
facebook | OAuth | Managed Keys | |
| Threads | threads | OAuth | Managed Keys |
| TikTok | tiktok | OAuth | Managed Keys |
| YouTube | youtube | OAuth | Managed Keys |
pinterest | OAuth | Managed Keys | |
| Google Business Profile | google_business | OAuth | BYOK required |
| Vimeo | vimeo | OAuth | BYOK required |
reddit | OAuth | BYOK required | |
| Bluesky | bluesky | App password | Managed Keys |
All platforms except Bluesky connect through the same OAuth flow (see the quickstart below). Bluesky uses an app password instead of OAuth. The authorize URL is identical for every network, but what the callback hands back is not - see the three callback contracts before you build against it.
Managed Keys are included with your subscription - we run the OAuth app, so you can connect an account without registering anything. BYOK required means Outstand ships no managed app for that network: you must register your own application and add its credentials before connecting an account. On any Managed Keys network you can still bring your own app if you want your brand on the OAuth screen. See Configurations for per-network setup guides.
Creating an account
To sign up for an account, you can go to the Outstand website and click on the "Sign Up" button.
Creating an account is free, no credit card is required and no invoices or other usage based billing is incurred.
Get an API Key
After signing up, you will be redirected to the main app dashboard.
From the dashboard, you can generate an API key.
This API key is used to authenticate your requests to the Outstand API.
Using the API
To use the API, you can pass the API key in the Authorization header of your requests.
Read more about authentication options and how to use your API key in the authentication section.
Developer Quickstart
Everything you need to go from zero to a published post on one page: connect an account,
upload media, and create a post. Every request uses Authorization: Bearer YOUR_API_KEY.
Estimated integration timeline
Outstand replaces per-platform SDKs, OAuth apps, and publishing quirks with one API, so a first integration is fast. A realistic timeline for a small team:
| Milestone | Typical effort |
|---|---|
| Sign up, generate an API key, first authenticated call | ~5 minutes |
| Connect your first social account via OAuth | ~30 minutes |
| Publish your first post | under 1 hour |
| Add media upload + scheduling | ~half a day |
| Production-ready (outcome polling, retries, error handling) | 1–2 days |
Most teams have a working prototype in an afternoon and ship to production within two days.
1. Connect an account
Connecting is a browser OAuth redirect - you send the user to Outstand's authorize URL for a network, they approve, and Outstand calls you back with the new account. Start the flow by sending the user to:
https://www.outstand.so/app/api/socials/{network}/{orgId}?redirect_uri=https://yourapp.com/callbackReplace {network} with a value from Supported Platforms (e.g. x)
and {orgId} with your organization ID. After the user approves, Outstand redirects them back to
your redirect_uri with the result on the query string. On failure the callback carries an
error param instead of success - handle both.
Check what your network sends back before you build on it, because it is not the same everywhere. There are three callback contracts:
| What comes back | Networks |
|---|---|
success, account_id, network_unique_id, username | x, instagram, tiktok, youtube, vimeo |
success and error only - no id on the query string | pinterest, reddit, threads |
A session token - the account does not exist yet | linkedin, facebook, google_business |
X, Instagram, TikTok, YouTube and Vimeo return all four params, and the account exists by the time the user lands back on you:
https://yourapp.com/callback?success=true&account_id=Kx7vQ&network_unique_id=1780…&username=brandPinterest, Reddit and Threads return success and error only. The account is created, but
its id is not on the query string - read it back from GET /v1/social-accounts after the
redirect.
LinkedIn, Facebook and Google Business Profile return a session token instead of an account.
One login on these networks can carry several pages or locations, so the choice of which to
connect is still outstanding when the user comes back. Completing it is a POST to
/app/api/socials/{network}/{orgId}/finalize with that token and the selected page ids.
Store the account_id you end up with - it's how you target this account when posting, and it is
an opaque string you should persist and pass back exactly as received.
Bluesky doesn't use OAuth; connect it with an app password:
curl -X POST https://api.outstand.so/v1/social-accounts/bluesky \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "brand.bsky.social",
"app_password": "xxxx-xxxx-xxxx-xxxx"
}'Confirm your connected accounts any time:
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"2. Upload media
Media upload is a two-step presigned-URL flow: ask for an upload URL, then PUT the file to
it, then confirm. Uploaded files live in storage for 60 days.
# Step 1 - request an upload URL
curl -X POST https://api.outstand.so/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "launch.jpg", "content_type": "image/jpeg" }'{
"success": true,
"data": {
"id": "Vn4kP",
"upload_url": "https://…r2…/kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe/7c9a1b42-6d3e-4f18-9a52-2b7c4e8d1f03/launch.jpg?X-Amz-Signature=…",
"expires_in": 3600
}
}# Step 2 - PUT the raw file bytes to the returned upload_url
curl -X PUT "PASTE_UPLOAD_URL_HERE" \
-H "Content-Type: image/jpeg" \
--data-binary @launch.jpg
# Step 3 - confirm the upload to activate it
curl -X POST https://api.outstand.so/v1/media/{mediaId}/confirm \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "size": 82451 }'The confirm response returns the public url you attach to a post:
{
"id": "Vn4kP",
"filename": "launch.jpg",
"url": "https://media.outstand.so/kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe/7c9a1b42-6d3e-4f18-9a52-2b7c4e8d1f03/launch.jpg",
"content_type": "image/jpeg",
"size": 82451,
"status": "active",
"expires_at": "2026-09-07T12:00:00Z"
}3. Create a post
Publish to one or more platforms in a single call. accounts is required. Each entry is
either the id or the exact username of a connected account, both of which come from
GET /v1/social-accounts (step 2). Account IDs are opaque - copy the one the API returned
rather than constructing one. Network names like "x" or "linkedin" are not accepted.
Prefer the id; see Targeting accounts for why. Attach media by passing
its url and a filename on the container:
curl -X POST https://api.outstand.so/v1/posts/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"containers": [
{
"content": "Our Q2 launch is live 🚀",
"media": [
{ "url": "https://media.outstand.so/kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe/7c9a1b42-6d3e-4f18-9a52-2b7c4e8d1f03/launch.jpg", "filename": "launch.jpg" }
]
}
],
"accounts": ["YOUR_X_ACCOUNT_ID", "YOUR_LINKEDIN_ACCOUNT_ID"]
}'{
"success": true,
"post": {
"id": "9dyJS",
"publishedAt": null,
"scheduledAt": null,
"socialAccounts": [
{ "id": "Kx7vQ", "nickname": "Brand X", "network": "x", "username": "brand" },
{ "id": "Tm2bN", "nickname": "Brand LI", "network": "linkedin", "username": "brand" }
],
"containers": [
{ "id": "3Rb8d", "content": "Our Q2 launch is live 🚀", "media": [{ "id": 1, "url": "https://media.outstand.so/kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe/7c9a1b42-6d3e-4f18-9a52-2b7c4e8d1f03/launch.jpg", "filename": "launch.jpg" }] }
]
}
}To schedule instead of publishing now, add a scheduledAt timestamp - see
Building a Social Media Scheduler below.
Quick Start
Once you have your API key, you can start using the Outstand API right away. Below are examples covering the most common workflows.
List Connected Social Accounts
Retrieve all social accounts connected to your organization:
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"Each row carries an id - that is the identifier you pass to accounts when posting:
{
"data": [
{ "id": "Kx7vQ", "nickname": "Brand X", "network": "x", "username": "brand", "isActive": 1 },
{ "id": "Tm2bN", "nickname": "Brand LI", "network": "linkedin", "username": "brand", "isActive": 1 }
]
}Targeting accounts
The accounts array takes account identifiers, and exactly two forms resolve:
- An account ID - the opaque
idfromGET /v1/social-accounts, e.g."Kx7vQ". This is the form you should use. - An account's exact username - e.g.
"brand". Matched by exact equality.
Nothing else resolves. In particular:
- Network names are not identifiers.
"x","linkedin","facebook"and the rest are rejected. To post to every X account you own, list their IDs. - Nicknames are not identifiers either, even though accounts have a
nicknamefield and it often happens to equal the network name. - Account IDs are opaque. Pass back exactly what the API returned. Never construct one and never assume a prefix or a format.
Two behaviours to know before you build against this:
Unresolvable entries are dropped silently. The request only fails if none of the
identifiers resolve. ["Kx7vQ", "linkedin"] returns success: true and publishes to Kx7vQ
alone - "linkedin" is discarded with no error and no entry in warnings. Check
post.socialAccounts in the response against what you asked for.
Usernames fan out. Username matching has no network filter and no active-account filter, and usernames are not unique across networks. One username can resolve to several accounts on different networks, including disconnected ones. Use IDs if you need to know exactly what you are publishing to.
Create and Publish a Post
Post to multiple platforms with a single API call. Every entry in the accounts array is
the id or the exact username of a connected account, taken from the
GET /v1/social-accounts response above - list first, then paste. If no entry matches a
connected account the call is rejected with 400 No social accounts found matching the provided account identifiers and nothing is created. If some entries match and others do not, the
unmatched ones are dropped silently and the post is created against the rest - see
Targeting accounts:
curl -X POST https://api.outstand.so/v1/posts/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hello from Outstand! 🚀",
"accounts": ["YOUR_X_ACCOUNT_ID", "YOUR_LINKEDIN_ACCOUNT_ID"]
}'Schedule a Post
Use the scheduledAt field to publish at a specific time in the future:
curl -X POST https://api.outstand.so/v1/posts/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Scheduled post from Outstand",
"accounts": ["YOUR_ACCOUNT_ID"],
"scheduledAt": "2026-04-01T09:00:00Z"
}'Building a Social Media Scheduler
A scheduler is one of the most common things developers build on top of Outstand. This section walks through the whole loop end to end - authenticate, schedule posts into your own queue, publish, and interpret the outcomes - with copy-paste code for each step.
How it works
Outstand is stateless about your scheduling logic and stateful about delivery. You
decide when each post should go out (your cron, your calendar, your posting slots) and
hand Outstand a single scheduledAt timestamp per post. Outstand stores the post, holds it,
and publishes it at that time - fanning out to every target account and tracking a
publish outcome per account.
The mental model, in four steps:
- Authenticate every request with your API key (
Authorization: Bearer …). - Schedule posts by creating them with a future
scheduledAt. Each post is your queue entry. There is no separate "queue" resource - a scheduled post is a queued post. - Poll the posts list to see what is coming up or has already gone out.
- Interpret outcomes by reading each post's per-account
status(pending,published,failed,deleted) and theerror/platformPostIdfields.
Scheduling rules:
scheduledAtis an ISO 8601 timestamp (UTC recommended, e.g.2026-04-01T09:00:00Z). Omit it - or pass a time in the past - and the post publishes immediately. The furthest you can schedule is 30 days into the future; a later timestamp is rejected with a400and no post is created. 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.
Step 1 - Authenticate
Every request carries your API key as a Bearer token. A quick call to
/v1/social-accounts both verifies the key and returns the accounts you can post to:
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"A missing or invalid key returns 401 with { "error": "Missing or invalid Authorization header" }.
Step 2 - Schedule a post into your queue
Create a post with a future scheduledAt. This is how you enqueue. The accounts array is
required and controls the fan-out; to attach a first comment (where the network supports it)
add extra containers - the first container is the post, each additional container is
published as a reply to it.
curl -X POST https://api.outstand.so/v1/posts/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"containers": [
{
"content": "Our Q2 launch is live 🚀 Here is everything that shipped.",
"media": [
{ "url": "https://cdn.example.com/launch.jpg", "filename": "launch.jpg" }
]
},
{ "content": "Full changelog in the thread 👇 https://example.com/changelog" }
],
"accounts": ["YOUR_X_ACCOUNT_ID", "YOUR_LINKEDIN_ACCOUNT_ID", "YOUR_INSTAGRAM_ACCOUNT_ID"],
"scheduledAt": "2026-04-01T09:00:00Z"
}'The response confirms the queued post and echoes back the resolved accounts and containers:
{
"success": true,
"post": {
"id": "9dyJS",
"orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
"publishedAt": null,
"scheduledAt": "2026-04-01T09:00:00Z",
"isDraft": false,
"createdAt": "2026-03-20T12:00:00Z",
"socialAccounts": [
{ "id": "Kx7vQ", "nickname": "Brand X", "network": "x", "username": "brand" },
{ "id": "Tm2bN", "nickname": "Brand LI", "network": "linkedin", "username": "brand" },
{ "id": "Rp9wL", "nickname": "Brand IG", "network": "instagram", "username": "brand" }
],
"containers": [
{ "id": "3Rb8d", "content": "Our Q2 launch is live 🚀 ...", "media": [{ "id": 1, "url": "https://cdn.example.com/launch.jpg", "filename": "launch.jpg" }] },
{ "id": "8Wq4c", "content": "Full changelog in the thread 👇 ...", "media": [] }
]
}
}Save post.id - it is the handle you use to check status, cancel, or reschedule.
Step 3 - List what's queued
Read your queue with GET /v1/posts. Filter by scheduled-time window to show only what is
upcoming, and page with limit / offset:
curl -X GET "https://api.outstand.so/v1/posts?scheduled_after=2026-03-20T00:00:00Z&scheduled_before=2026-04-30T00:00:00Z&limit=50&offset=0" \
-H "Authorization: Bearer YOUR_API_KEY"Available query params: scheduled_after, scheduled_before, created_after,
created_before (all ISO 8601), social_account_id, limit (1–100, default 50), and
offset. Results are ordered newest-created first inside a pagination envelope:
{
"success": true,
"data": [ /* posts */ ],
"posts": [ /* same posts, for backward compatibility */ ],
"pagination": { "limit": 50, "offset": 0, "total": 12 }
}A post is queued/pending while scheduledAt is set and publishedAt is still null.
Once it goes out, publishedAt is populated.
Step 4 - Interpret the outcome
Fetch a single post to see the per-account result. Outstand publishes to each target
independently, so a post can partially succeed - read status on every entry rather than
assuming the post as a whole passed or failed:
curl -X GET https://api.outstand.so/v1/posts/9dyJS \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"post": {
"id": "9dyJS",
"publishedAt": "2026-04-01T09:00:03Z",
"scheduledAt": "2026-04-01T09:00:00Z",
"socialAccounts": [
{ "id": "Kx7vQ", "network": "x", "username": "brand", "status": "published", "platformPostId": "1780000000000000000", "error": null, "publishedAt": "2026-04-01T09:00:03Z" },
{ "id": "Tm2bN", "network": "linkedin", "username": "brand", "status": "failed", "platformPostId": null, "error": "Token expired - reconnect the account", "publishedAt": null }
],
"containers": [
{
"id": "8Wq4c",
"content": "Full changelog in the thread 👇 ...",
"publishResults": [
{ "accountId": "Kx7vQ", "status": "published", "platformCommentId": "1780000000000000001", "error": null, "publishedAt": "2026-04-01T09:00:05Z" }
]
}
]
}
}Per-account status values:
| Status | Meaning |
|---|---|
pending | Created/scheduled, awaiting publish. This is your "queued" state. |
published | Delivered successfully - platformPostId holds the native post ID. |
failed | Publish failed - read error for the reason (e.g. expired token). |
deleted | The post was removed from the platform. |
First comments carry their own outcomes in each container's publishResults array
(status of published or failed, plus platformCommentId / error).
Reschedule or cancel a queued post
There is no in-place reschedule endpoint. To move a scheduled post, cancel it and create a new one with the new time. Cancelling deletes the pending post and removes it from the publishing pipeline:
curl -X DELETE https://api.outstand.so/v1/posts/9dyJS \
-H "Authorization: Bearer YOUR_API_KEY"Full scheduler loop in TypeScript
A minimal scheduler: resolve accounts, enqueue a post at a slot time, then poll until every account resolves to a terminal outcome.
const API_KEY = process.env.OUTSTAND_API_KEY!;
const BASE_URL = 'https://api.outstand.so';
const auth = { Authorization: `Bearer ${API_KEY}` };
// 1. Authenticate + discover accounts
const accountsRes = await fetch(`${BASE_URL}/v1/social-accounts`, { headers: auth });
const { data: accounts } = await accountsRes.json();
// 2. Schedule a post into your queue (a slot your own scheduler decided on)
const scheduledAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour out
const createRes = await fetch(`${BASE_URL}/v1/posts/`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
content: 'Scheduled with Outstand ⏰',
accounts: accounts.map((a: any) => a.id),
scheduledAt,
}),
});
const { post } = await createRes.json();
console.log('Queued post:', post.id, 'for', post.scheduledAt);
// 3. Later: poll the outcome and interpret per-account status
const outcomeRes = await fetch(`${BASE_URL}/v1/posts/${post.id}`, { headers: auth });
const { post: result } = await outcomeRes.json();
for (const acc of result.socialAccounts) {
if (acc.status === 'published') {
console.log(`✅ ${acc.network}: ${acc.platformPostId}`);
} else if (acc.status === 'failed') {
console.error(`❌ ${acc.network}: ${acc.error}`);
} else {
console.log(`⏳ ${acc.network}: still ${acc.status}`);
}
}Full scheduler loop in Python
The same flow using Python and the requests library:
import os
import requests
from datetime import datetime, timedelta, timezone
API_KEY = os.environ["OUTSTAND_API_KEY"]
BASE_URL = "https://api.outstand.so"
auth = {"Authorization": f"Bearer {API_KEY}"}
# 1. Authenticate + discover accounts
accounts = requests.get(f"{BASE_URL}/v1/social-accounts", headers=auth).json()["data"]
# 2. Schedule a post into your queue
scheduled_at = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
post = requests.post(
f"{BASE_URL}/v1/posts/",
headers={**auth, "Content-Type": "application/json"},
json={
"content": "Scheduled with Outstand ⏰",
"accounts": [a["id"] for a in accounts],
"scheduledAt": scheduled_at,
},
).json()["post"]
print(f"Queued post {post['id']} for {post['scheduledAt']}")
# 3. Later: poll the outcome and interpret per-account status
result = requests.get(f"{BASE_URL}/v1/posts/{post['id']}", headers=auth).json()["post"]
for acc in result["socialAccounts"]:
if acc["status"] == "published":
print(f"✅ {acc['network']}: {acc['platformPostId']}")
elif acc["status"] == "failed":
print(f"❌ {acc['network']}: {acc['error']}")
else:
print(f"⏳ {acc['network']}: still {acc['status']}")Pricing & ROI
Business for agencies: $129/month per organization includes 50,000 posts per billing month, then $0.005 per additional post. Business explicitly permits reselling and includes one 30-minute integration-planning call per organization, arranged through support, plus priority handling through existing support channels without a response-time guarantee. Accounts start at 1,000 slots, with free increases through support. Managed Keys and BYOK are included. The Unlimited Posting add-on and automatic comeback/retention offers are not available on Business. Other plans require written permission to resell.
The examples below describe the existing Pay as you go plan.
Outstand bills on a simple model: a flat monthly fee that includes a bundle of posts, plus metered pricing for anything beyond it. The billing unit is one post delivered to one connected account, so a post that fans out to three accounts counts as three. There are no per-platform fees and no separate charge for scheduling, first comments or per-network content overrides.
The figures below reflect the public pricing calculator and are here to help you estimate quickly. See our landing page for authoritative, current numbers.
Illustrative model: $19/mo base including 3,000 posts, then $0.007/post for posts 3,001–10,000, and $0.005/post beyond 10,000.
Worked examples
| Monthly posts | How it's billed | Est. monthly cost | Effective cost / post |
|---|---|---|---|
| 1,000 | Within the 3,000 included | $19.00 | $0.019 |
| 10,000 | $19 + 7,000 × $0.007 | $68.00 | $0.0068 |
| 100,000 | $19 + 7,000 × $0.007 + 90,000 × $0.005 | $518.00 | $0.0052 |
The more you publish, the cheaper each post gets. For a small startup that means:
- Prototyping (≈1k posts/mo): stays inside the base plan at $19/mo - effectively free to validate an idea across every platform.
- Scaling (≈10k posts/mo): ~$68/mo, still less than the salary-hours it takes to build and maintain a single platform integration in-house.
- High volume (≈100k posts/mo): ~$518/mo, or about half a cent per post, all-in, across all supported networks.
The ROI case is the integration you don't build: one Outstand integration replaces twelve separate OAuth apps, SDKs, and review processes - each of which can take weeks to build and requires ongoing maintenance as platform APIs change.