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:

Platformnetwork valueConnect viaCredentials
X (Twitter)xOAuthBYOK required
LinkedInlinkedinOAuthManaged Keys
InstagraminstagramOAuthManaged Keys
FacebookfacebookOAuthManaged Keys
ThreadsthreadsOAuthManaged Keys
TikToktiktokOAuthManaged Keys
YouTubeyoutubeOAuthManaged Keys
PinterestpinterestOAuthManaged Keys
Google Business Profilegoogle_businessOAuthBYOK required
VimeovimeoOAuthBYOK required
RedditredditOAuthBYOK required
BlueskyblueskyApp passwordManaged 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:

MilestoneTypical effort
Sign up, generate an API key, first authenticated call~5 minutes
Connect your first social account via OAuth~30 minutes
Publish your first postunder 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/callback

Replace {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 backNetworks
success, account_id, network_unique_id, usernamex, instagram, tiktok, youtube, vimeo
success and error only - no id on the query stringpinterest, reddit, threads
A session token - the account does not exist yetlinkedin, 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=brand

Pinterest, 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:

  1. An account ID - the opaque id from GET /v1/social-accounts, e.g. "Kx7vQ". This is the form you should use.
  2. 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 nickname field 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:

  1. Authenticate every request with your API key (Authorization: Bearer …).
  2. 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.
  3. Poll the posts list to see what is coming up or has already gone out.
  4. Interpret outcomes by reading each post's per-account status (pending, published, failed, deleted) and the error / platformPostId fields.

Scheduling rules: scheduledAt is 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 a 400 and 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:

StatusMeaning
pendingCreated/scheduled, awaiting publish. This is your "queued" state.
publishedDelivered successfully - platformPostId holds the native post ID.
failedPublish failed - read error for the reason (e.g. expired token).
deletedThe 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 postsHow it's billedEst. monthly costEffective cost / post
1,000Within 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.