# Approve or ignore a pending reply (https://www.outstand.so/docs/approve-or-ignore-a-pending-reply)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Approve or ignore a reply pending approval on a Threads post managed by Outstand. Provide Outstand's post ID in the path and the pending reply's Threads-specific ID as replyId, and set `approve` in the body (true to approve, false to ignore). Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
## API Endpoint
`POST /v1/threads/posts/{postId}/pending-replies/{replyId}`
**Summary:** Approve or ignore a pending reply
Approve or ignore a reply pending approval on a Threads post managed by Outstand. Provide Outstand's post ID in the path and the pending reply's Threads-specific ID as replyId, and set `approve` in the body (true to approve, false to ignore). Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
**Tags:** Threads, Replies
## Parameters
- **postId** (path: string) [required]
- **replyId** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"approve": {
"type": "boolean",
"description": "Set to true to approve the pending reply, or false to ignore it.",
"example": true
},
"platform_post_id": {
"type": "string",
"description": "The platform-specific post ID of the account whose credentials should be used. Optional when the post was published to only one account.",
"example": "123456789"
},
"account_username": {
"type": "string",
"description": "The username or nickname of the connected account whose credentials should be used. Optional when the post was published to only one account.",
"example": "mycompany"
}
},
"required": [
"approve"
],
"description": "Manage pending reply request"
}
```
## Responses
### 200
Pending reply updated
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if the pending reply was successfully approved or ignored.",
"example": true
}
},
"required": [
"success"
],
"description": "Manage pending reply response"
}
```
### 400
Invalid request
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post or matching social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/threads/posts/{postId}/pending-replies/{replyId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"approve": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/threads/posts/{postId}/pending-replies/{replyId}', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"approve": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
})
});
const data = await response.json();
```
# How It Works: Architecture (https://www.outstand.so/docs/architecture)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
Outstand is a social media API aggregator: you integrate once against a single API, and Outstand fans each request out to every downstream social platform (X, LinkedIn, Instagram, Facebook, TikTok, YouTube, Threads, Pinterest, Bluesky, Google Business, Vimeo, Reddit, and more). This page explains the architecture - how a single `create post` call becomes many platform-specific publishes - so you can reason about latency, consistency, and failure handling in your integration.
## What "aggregator" means here
A social media API aggregator collapses N platform integrations into one. Instead of learning ten OAuth flows, ten payload formats, ten media-upload dances, and ten rate-limit regimes, you send **one normalized request** to Outstand. Outstand owns the per-platform translation and delivery.
The core of the system is a **fan-out pipeline**: one inbound post is duplicated, adapted, and dispatched to each connected account independently. The rest of this document walks that pipeline stage by stage.
## The Fan-Out Pipeline
Every publish moves through four stages:
```
ingest ─▶ normalize ─▶ per-platform adaptor ─▶ send
```
### 1. Ingest
The client sends a single normalized request to the API gateway, which routes `/v1/posts` to the **posts API handler**. The handler validates the payload, then persists three things transactionally:
* The **post** record (content-level metadata, `scheduledAt`).
* One or more **post containers** - the normalized content units (text + attached media) that make up the post.
* The **post-to-account links** - the set of connected social accounts this post should fan out to.
At this point nothing has been sent to any platform. Ingest is intentionally cheap: its job is to durably record intent and hand off to the pipeline. The API responds immediately with a post ID (see the Synchronous vs Asynchronous section below).
### 2. Normalize
Normalization is what makes "integrate once" possible. Your inbound content is stored in a **platform-agnostic representation** - a post container holding text and an ordered list of media references. This canonical form is deliberately independent of any single platform's schema.
When publishing runs, the publishing service loads the post, its containers, and the list of target accounts, producing an in-memory normalized object that each adaptor consumes. Media is stored once and referenced by every account's adaptor rather than re-uploaded per request by the client.
### 3. Per-Platform Adaptor
Each connected account maps to a **provider adaptor** selected by network. Conceptually:
```
adaptor = selectAdaptor(account.network)
platformPostId = adaptor.publish(normalizedPost, account)
```
The adaptor is the per-platform translation layer. It takes the normalized post and turns it into exactly what that platform's API expects - the correct field names, media-upload sequence (e.g. Instagram's container-creation-then-poll, X's chunked media upload), character constraints, and authentication. Every adaptor implements the same contract: it receives all the parameters it needs and never touches the database or other platforms directly. This isolation is why one platform's quirks or outages can't corrupt another's publish.
Immediately before an adaptor sends, the pipeline ensures the account's access token is valid, refreshing short-lived tokens on demand (TikTok tokens, for example, last only \~24h and are refreshed inline before the send).
### 4. Send
The adaptor calls the platform's API and returns a **platform post ID** on success. The pipeline then records the outcome per account:
* **Success** → `status: published`, `platformPostId` populated, `publishedAt` set. The first success also stamps the post-level `publishedAt`.
* **Failure** → `status: failed`, `error` populated with the platform's message.
Crucially, each account is sent independently and a failure does **not** abort the others - see the Partial-Success Semantics section below.
## Synchronous vs Asynchronous Posting
Outstand's publish path is **asynchronous by design**.
| Aspect | Behavior |
| ----------------------------------- | ------------------------------------------------------------------------------------------- |
| API call (`POST /v1/posts`) | **Synchronous** - validates, persists, enqueues, and returns a post ID within milliseconds. |
| Actual publishing to platforms | **Asynchronous** - performed later by the publishing service, decoupled via a queue. |
| Immediate posts (no `scheduledAt`) | Enqueued with a `scheduleTime` of *now*; publishing typically begins within seconds. |
| Scheduled posts (`scheduledAt` set) | Enqueued with `scheduleTime = scheduledAt`; the queue holds the task until then (±30s). |
This split matters for your integration: a `200` from `POST /v1/posts` means *"accepted and durably queued"*, **not** *"live on every platform."* To learn the actual per-account outcome you either poll `GET /v1/posts/{id}` or, preferably, subscribe to [webhooks](/webhooks). See the [Post Lifecycle](/post-lifecycle) guide for the status fields.
Why asynchronous? Publishing to a single platform can take anywhere from \~1s to \~10s (media upload, container polling, third-party latency). A post fanning out to eight accounts could otherwise block the client for a minute or more and would be at the mercy of the slowest platform. Decoupling ingest from send keeps the client-facing API fast and predictable while absorbing downstream variability.
## Queuing and Backpressure
The decoupling between ingest and send is provided by a **managed task queue**. Ingest enqueues one publishing task per post; a separate queue handles webhook delivery.
The queue does three jobs:
1. **Time-shifting.** Each task carries a `scheduleTime`. Immediate posts run now; scheduled posts are held by the queue until their moment. No cron-polling loop is required.
2. **Backpressure.** The queue enforces dispatch limits - maximum dispatches per second and maximum concurrent in-flight tasks. During a spike (say, thousands of scheduled posts firing at the top of the hour), tasks queue up and drain at a controlled rate instead of overwhelming the publishing service or the downstream platform APIs. Producers (the ingest path) never block on consumers.
3. **Durability.** An enqueued task survives service restarts and transient outages. If the publishing service is briefly unavailable, the queue holds and redelivers.
Because backpressure lives in the queue rather than in the client-facing API, your `POST /v1/posts` latency stays flat even when the system is draining a large scheduled backlog.
## Partial-Success Semantics
Multi-platform fan-out is **not** all-or-nothing. Each account in a post is published independently, and the pipeline deliberately continues past failures:
```
for account in post.accounts:
try:
platformPostId = adaptor(account).publish(normalizedPost, account)
markPublished(account, platformPostId)
catch error:
markFailed(account, error)
# continue to the next account instead of aborting the batch
```
This yields three possible post-level outcomes:
| Outcome | Condition | Post-level effect | Webhook |
| ------------------- | --------------------------- | --------------------------------- | ------------------------------------- |
| **Full success** | Every account published | `publishedAt` set | `post.published` |
| **Partial success** | Some published, some failed | `publishedAt` set (first success) | `post.published` (payload lists both) |
| **Full failure** | Every account failed | `publishedAt` stays `null` | `post.error` |
The single `post.published` event fires when **at least one** account succeeds; its payload enumerates every account with either a `platformPostId` (success) or an `error` (failure), so a partial outcome is fully observable in one notification. `post.error` fires only when the entire fan-out failed.
**Integration implication:** always inspect per-account `status` rather than treating the post as a single boolean. A post that "published" may still have a failed account - for example an expired Facebook token - while its other accounts went live. See [Post Lifecycle](/post-lifecycle) for the exact fields.
## Rate Limiting and Retries
Rate limiting and retries operate at two layers, which together protect both Outstand and the downstream platforms.
### Queue-level (transport)
The task queue applies its own retry policy to each publishing and webhook task. If a task's handler returns a retryable failure, the queue **re-dispatches it with exponential backoff** up to a configured maximum, without any client involvement. Combined with the dispatch-rate and concurrency caps described under Queuing and Backpressure above, this smooths bursts so downstream platform APIs are approached at a sustainable rate rather than in a thundering herd.
### Adaptor-level (platform)
Individual adaptors handle platform-specific timing directly. Where a platform requires an asynchronous publish (e.g. Instagram and X media containers), the adaptor **polls the platform for readiness** with bounded attempts before completing the send. When a platform returns a rate-limit or auth error, the adaptor surfaces a structured error (`rate_limited`, `unauthorized`, `forbidden`, `network_error`) that is recorded on the account and reflected in the health-check surface.
### What retries mean for you
* **Transient transport failures** (cold start, brief network blip) are retried automatically by the queue - you don't need to resubmit.
* **Terminal platform failures** (invalid media, expired token, content rejected) are recorded as `failed` on that account and are **not** silently retried into a duplicate post. To republish after fixing the cause, delete the failed post and create a new one - see the retry guidance in [Post Lifecycle](/post-lifecycle#best-practices).
* **Idempotency:** because a successful send records the account as `published`, re-publishing logic keys off per-account status to avoid double-posting to a platform that already succeeded.
## Performance and SLA Implications
A few properties fall out of this architecture that are worth designing around:
* **API responsiveness is decoupled from platform latency.** `POST /v1/posts` returns in milliseconds regardless of how many platforms the post targets or how slow they are, because the send happens off the request path.
* **End-to-end publish time is bounded by the slowest platform in the fan-out**, plus queue dispatch delay. Accounts within a post are processed one after another, so a post with many accounts, or one that includes a slow media upload, takes proportionally longer to fully go live. Budget seconds, not milliseconds, for full fan-out completion.
* **Scheduled accuracy is approximately ±30 seconds** of `scheduledAt`, governed by queue dispatch timing.
* **Spikes degrade gracefully, not catastrophically.** Backpressure means a large simultaneous scheduled batch drains at a controlled rate; individual posts may see slightly higher queue latency under load, but the ingest API and unrelated traffic are unaffected.
* **Failure is isolated.** A single platform outage or a single expired token affects only that account's send, never the post as a whole and never other accounts.
For real-time confirmation of when content actually goes live, rely on [webhooks](/webhooks) rather than the synchronous API response; for after-the-fact inspection, poll `GET /v1/posts/{id}`.
## Summary
| Concern | How Outstand handles it |
| ------------------------------- | ------------------------------------------------------------- |
| One integration, many platforms | Normalized request → per-platform adaptors |
| Client latency | Synchronous ingest, asynchronous send |
| Scheduling | `scheduleTime` on queued tasks |
| Load spikes | Queue backpressure (rate + concurrency caps) |
| Multi-platform failures | Independent per-account send, partial success |
| Transient errors | Automatic queue retries with backoff |
| Platform-specific timing | Adaptor-level polling and structured errors |
| Observability | Per-account status + `post.published` / `post.error` webhooks |
# Authentication (https://www.outstand.so/docs/authentication)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
A valid API key is required to use the Outstand API. In this section, you can find information on how to obtain an API key as well as how to use it.
## Authentication Methods
We currently support only one authentication method: API key.
## Obtaining an API Key
To obtain an API key, you can go to the [Outstand website](https://www.outstand.so/app/signup) and click on the "Get API Key" button.
The generated API key is unique for your user account, even if more members belong to your organization.
Be ready to copy the API key to a secure location, as you won't be able to see it again after generating it.
## Using an API Key
To use an API key, you can pass it in the `Authorization` header of your requests.
```
Authorization: Bearer
```
## Example Request
A successful authenticated request looks like this:
```bash
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer sk_live_abc123def456"
```
Response:
```json
{
"success": true,
"data": [
{
"id": "Kx7vQ",
"network": "x",
"username": "mycompany"
}
]
}
```
If the API key is missing or invalid, you will receive an error response:
```json
{
"success": false,
"error": "Invalid or missing API key"
}
```
## Subscription Required
Authenticating successfully is not enough on its own: the organization that owns the API key must
also have an active subscription. If it does not, every API endpoint responds with
`402 Payment Required`:
```json
{
"success": false,
"error": "Your organization does not have an active subscription.",
"code": "subscription_inactive",
"billingUrl": "https://www.outstand.so/app/settings/billing"
}
```
Only the `active` subscription status grants API access. Any other status - including `trialing`,
`past_due`, `unpaid`, `incomplete` and `canceled` - results in a `402`.
Treat a `402` as non-retryable: retrying the request will keep failing until the subscription is
reactivated in [billing settings](https://www.outstand.so/app/settings/billing). Once it is active
again, access is restored automatically with no further action needed.
## Rate Limits
We use dynamic rate limits based on your post traffic and the number of accounts you have connected,
so there is no single published number to code against - your limit scales with how you actually use
the API.
Because of that, read your current limit from the response headers rather than hardcoding it. Every
successful request on a rate limited key describes the window it belongs to:
| Header | Meaning |
| ----------------------- | --------------------------------------------------- |
| `X-RateLimit-Limit` | Maximum requests allowed within the current window |
| `X-RateLimit-Remaining` | Requests still available in the current window |
| `X-RateLimit-Reset` | Unix timestamp (seconds) at which the window resets |
Pace your requests off `X-RateLimit-Remaining` and you should never hit the limit. If you do, the
request is rejected with `429 Too Many Requests`:
```json
{
"error": "API key rate limit exceeded",
"details": "Rate limit exceeded."
}
```
A `429` is retryable. Unlike a `401`, it does not mean your key is wrong - there is no need to
rotate it. When the response carries a `Retry-After` header, wait that many seconds before trying
again; otherwise back off exponentially.
Some keys carry a fixed request quota instead of a rolling window. When that quota runs out the
response is also a `429`, but with `"error": "API key request quota exhausted"`. Waiting will not
help in that case - [get in touch](mailto:contact@outstand.so) to have the quota raised or refilled.
## Multiple API Keys
You can issue multiple API keys for your organization, each with a different name.
It's a good practice to create a new API key for each project or application you are working on -
as well as use separate API keys for development and production environments.
# Backend Integration Guide (https://www.outstand.so/docs/backend-integration)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
This guide explains how to integrate Outstand into your backend and manage social media IDs in your database. By the end, you'll have a clear picture of the data flow and a recommended schema for storing Outstand resources.
## Integration Overview
## Key Concepts
Outstand has three main resources you need to track in your backend:
| Resource | What it represents | When you get it |
| ------------------ | ------------------------------------------------------------------ | ---------------------------------------- |
| **Social Network** | Your OAuth app configuration (e.g., your X app, your Facebook app) | When you call `POST /v1/social-networks` |
| **Social Account** | A user's connected account (e.g., @john on X) | When a user completes the OAuth flow |
| **Post** | A piece of content published to one or more accounts | When you call `POST /v1/posts` |
## Step-by-Step Integration
### 1. Store your OAuth credentials
Register your OAuth app credentials with Outstand. You typically do this once per social platform.
```bash
curl -X POST https://api.outstand.so/v1/social-networks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"network": "x",
"client_key": "your_oauth_client_id",
"client_secret": "your_oauth_client_secret"
}'
```
**Save the returned `id`** - this is your `socialNetworkId`. Store it in your database mapped to the platform name.
### 2. Connect user accounts
When a user wants to connect their social account, get an authorization URL and redirect them. The path segment is the network name, not the `socialNetworkId` you stored above:
```bash
curl -X POST https://api.outstand.so/v1/social-networks/instagram/auth-url \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"redirect_uri": "https://yourapp.com/social/connected"
}'
```
After the user authorizes, Outstand handles the OAuth callback. The new social account becomes available via the API (and via webhook if configured).
### 3. Retrieve and store social account IDs
After a user connects their account, list your social accounts to get the new entry:
```bash
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Save the returned `id`** for each account. This `socialAccountId` is what you'll use to create posts.
### 4. Create posts
Use the stored `socialAccountId`s to publish content:
```bash
curl -X POST https://api.outstand.so/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"containers": [
{ "content": "Hello world!" }
],
"accounts": ["STORED_SOCIAL_ACCOUNT_ID", "ANOTHER_STORED_SOCIAL_ACCOUNT_ID"]
}'
```
Target accounts with the `accounts` array, referencing each connected account by the `socialAccountId` you stored in step 3 or by the account's exact `username`. These identifiers are opaque - store and replay what the API returned, never build one. Network names (`x`, `linkedin`, …) and nicknames are not identifiers and do not resolve. An entry that resolves to nothing is dropped silently as long as at least one other entry resolves, so compare the response's `socialAccounts` against what you sent. See [Targeting accounts](/docs/getting-started#targeting-accounts). Attach media by adding `media` objects (`{ url, filename }`) to a container - media is referenced by its uploaded URL, not by id.
**Save the returned `postId`** and the per-account `platformPostId`s for tracking publish status and analytics.
## Recommended Database Schema
Here's a recommended schema for storing Outstand resource IDs alongside your own data. Adapt this to your ORM and database.
### SQL Example
```sql
-- Map your users/orgs to Outstand social networks
CREATE TABLE social_networks (
id SERIAL PRIMARY KEY,
outstand_network_id VARCHAR(255) NOT NULL, -- ID from Outstand
platform VARCHAR(50) NOT NULL, -- 'x', 'facebook', 'instagram', etc.
created_at TIMESTAMP DEFAULT NOW()
);
-- Map connected accounts to your users
CREATE TABLE social_accounts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
outstand_account_id VARCHAR(255) NOT NULL, -- ID from Outstand
platform VARCHAR(50) NOT NULL,
username VARCHAR(255),
connected_at TIMESTAMP DEFAULT NOW()
);
-- Track posts and their publish status
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
outstand_post_id VARCHAR(255) NOT NULL, -- ID from Outstand
content TEXT,
scheduled_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- Track per-account publish results
CREATE TABLE post_accounts (
id SERIAL PRIMARY KEY,
post_id INTEGER NOT NULL REFERENCES posts(id),
outstand_account_id VARCHAR(255) NOT NULL,
platform_post_id VARCHAR(255), -- Native platform post ID
status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'published', 'failed'
error TEXT,
published_at TIMESTAMP
);
```
### TypeScript Example (Prisma)
```prisma
model SocialNetwork {
id String @id @default(cuid())
outstandNetworkId String @unique
platform String
createdAt DateTime @default(now())
}
model SocialAccount {
id String @id @default(cuid())
userId String
outstandAccountId String @unique
platform String
username String?
connectedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
model Post {
id String @id @default(cuid())
userId String
outstandPostId String @unique
content String?
scheduledAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
postAccounts PostAccount[]
}
model PostAccount {
id String @id @default(cuid())
postId String
outstandAccountId String
platformPostId String?
status String @default("pending")
error String?
publishedAt DateTime?
post Post @relation(fields: [postId], references: [id])
}
```
## Keeping Data in Sync
### Option A: Webhooks (Recommended)
Configure [webhooks](/docs/webhooks) to receive real-time updates. This is the best approach for keeping your database in sync:
* `post.published` - Update post status and store `platformPostId`
* `post.error` - Record errors for failed publishes
```typescript
// Example webhook handler
app.post('/webhooks/outstand', async (req, res) => {
const event = req.body;
switch (event.event) {
case 'post.published':
for (const account of event.data.socialAccounts) {
await db.postAccount.update({
where: { outstandAccountId: account.id },
data: {
status: account.error ? 'failed' : 'published',
platformPostId: account.platformPostId,
error: account.error ?? null,
publishedAt: account.publishedAt,
},
});
}
break;
case 'post.error':
for (const account of event.data.socialAccounts) {
await db.postAccount.update({
where: { outstandAccountId: account.id },
data: { status: 'failed', error: account.error },
});
}
break;
}
res.status(200).send('OK');
});
```
### Option B: Polling
If you can't use webhooks, poll the post status endpoint:
```typescript
async function checkPostStatus(outstandPostId: string) {
const res = await fetch(
`https://api.outstand.so/v1/posts/${outstandPostId}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { post } = await res.json();
for (const account of post.socialAccounts) {
await db.postAccount.update({
where: { outstandAccountId: account.id },
data: {
status: account.status,
platformPostId: account.platformPostId,
error: account.error,
publishedAt: account.publishedAt,
},
});
}
}
```
## Complete Integration Flow
## Best Practices
1. **Always store Outstand IDs**: Save `socialNetworkId`, `socialAccountId`, and `postId` in your database. These are stable identifiers you'll need for all subsequent API calls.
2. **Map accounts to your users**: Maintain a relationship between your user records and their connected social accounts. This lets you show users which accounts they have connected and target posts to specific accounts.
3. **Use webhooks over polling**: Webhooks give you real-time updates without the overhead of repeated API calls. See the [webhooks documentation](/docs/webhooks) for setup.
4. **Store platform post IDs**: The `platformPostId` returned after publishing is the native ID on the social platform (e.g., the tweet ID on X). Store this for analytics, deep linking, or comment management.
5. **Handle partial failures**: A post can succeed on some accounts and fail on others. Always check the per-account status rather than assuming all-or-nothing. See [Post Lifecycle](/docs/post-lifecycle) for details.
6. **Index Outstand IDs**: Add database indexes on columns storing Outstand IDs (`outstand_network_id`, `outstand_account_id`, `outstand_post_id`) for fast lookups when processing webhooks.
# Powered by Outstand Badge (https://www.outstand.so/docs/badge)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
If you like Outstand, you can show a **Powered by Outstand** badge on your site. It is entirely
optional - there is no discount, no plan requirement, and nothing changes about your account either
way. It just links back to us, and we appreciate it.
Everything is served from `static.outstand.so`. There is nothing to install and no API key involved.
## Preview the badges
Each badge is shown below on the kind of background it is designed for. Pick the one you like, then
grab it with the [HTML snippet](#html-snippet) (use the file name) or the [script tag](#script-tag)
(use the `data-variant`). Click any file name to open the raw SVG.
Standard - for dark sites
data-variant="standard"
badge-dark.svg - 181x34
Standard - for light sites
data-variant="standard"
badge-light.svg - 181x34
Pill - for dark sites (its outline pulses)
data-variant="pill"
pill-dark.svg - 155x30
Pill - for light sites
data-variant="pill"
pill-light.svg - 155x30
Powered by Outstand
Footer - for dark sites
data-variant="footer"
Script tag only (it is text, not an image)
Powered by Outstand
Footer - for light sites
data-variant="footer"
Script tag only (it is text, not an image)
The previews above load the real SVGs from `static.outstand.so`. If they do not appear, the badge
assets have not been published to the CDN yet.
## Pick an embed method
There are two ways to add the badge. **Use the HTML snippet unless you have a reason not to.**
| | HTML snippet | Script tag |
| --------------------------- | ----------------------------------------- | --------------------------------------------- |
| Works without JavaScript | Yes | No |
| Affected by your site's CSS | Yes - it is an ordinary link on your page | No - it renders inside a shadow root |
| Hover effect | No | Yes |
| Best for | Static sites, templates, footers | React/Vue/SPA, or when your CSS is aggressive |
The HTML snippet is a plain `` in your page, so your own stylesheet applies to it. If you have
broad global rules (for example `img { width: 100% }`), either scope them away from the badge or
use the script tag, which is immune to them.
## HTML snippet
Paste this wherever you want the badge. Swap `badge-dark.svg` for the variant you want from the
table further down.
```html
```
Keep the `width` and `height` attributes. They stop the badge from shifting your layout while it
loads, which protects your Cumulative Layout Shift score.
## Script tag
The badge is inserted immediately after the script tag, or into `data-target` if you provide one.
```html
```
Into a specific element:
```html
```
### Attributes
| Attribute | Values | Default | Notes |
| -------------- | ---------------------------- | ---------- | ------------------------------------------------------------ |
| `data-theme` | `dark`, `light`, `auto` | `dark` | Describes **your site**, not the badge. See below. |
| `data-variant` | `standard`, `pill`, `footer` | `standard` | See the variants below. |
| `data-target` | Any CSS selector | - | Where to render. If it matches nothing, nothing is rendered. |
`data-theme` describes the **background the badge sits on**, not the badge itself. A dark site
uses `data-theme="dark"` and gets the dark badge. This is the one thing people get backwards.
`data-theme="auto"` follows your visitor's operating system light/dark preference. That is only a
guess at what your page looks like, so it is a convenience rather than a correct answer - if your
site is always dark, say `data-theme="dark"` explicitly.
## Variants
All three are shown in [Preview the badges](#preview-the-badges) above.
**Standard** is the default: a bordered badge with the Outstand mark.
**Pill** is smaller and fully rounded, for tight spaces. On dark sites its outline pulses gently;
this is disabled automatically for visitors who have "reduce motion" enabled.
**Footer** is a small inline text lockup for sitting in a footer row next to a copyright line. It is
text rather than an image, so it is only available via the script tag:
```html
```
## Notes
* **Do not add `rel="nofollow"`.** The snippets above are what we ask for, and a followed link is the
entire point. Since the badge earns you nothing, it is a normal editorial link.
* **Do not rehost the SVGs.** Serving them from `static.outstand.so` means you pick up fixes
automatically, and the files are cached at the edge.
* **The links carry UTM parameters** so we can see how much traffic the badge sends. Keep them if you
can; the badge works without them either way.
* **Want to restyle it?** Please do not modify the artwork. If the badge does not fit your site,
[tell us](mailto:contact@outstand.so) and we will look at adding a variant.
# Check account health (https://www.outstand.so/docs/check-account-health)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Checks whether the stored access token for a social account is still valid by calling the network's identity endpoint.
If the token is expired (401), one automatic refresh is attempted and the new tokens are persisted before re-checking.
**Supported networks:** X, LinkedIn, Facebook, Instagram, Threads, TikTok, YouTube, Pinterest, Bluesky, Google Business, Vimeo, Reddit
## API Endpoint
`GET /v1/social-accounts/{id}/health`
**Summary:** Check account health
Checks whether the stored access token for a social account is still valid by calling the network's identity endpoint.
If the token is expired (401), one automatic refresh is attempted and the new tokens are persisted before re-checking.
**Supported networks:** X, LinkedIn, Facebook, Instagram, Threads, TikTok, YouTube, Pinterest, Bluesky, Google Business, Vimeo, Reddit
**Tags:** Social Accounts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Health check result (may be healthy: false)
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Social account ID",
"example": "9dyJS"
},
"network": {
"type": "string",
"description": "Social network name",
"example": "instagram"
},
"healthy": {
"type": "boolean",
"description": "Whether the stored token is currently valid",
"example": true
},
"checkedAt": {
"type": "string",
"description": "ISO timestamp of the check",
"example": "2024-01-01T00:00:00.000Z"
},
"details": {
"type": "object",
"additionalProperties": {},
"description": "Identity details from the network (e.g. username, id)"
},
"error": {
"type": "string",
"description": "Error message when unhealthy"
},
"errorCode": {
"type": "string",
"enum": [
"unauthorized",
"forbidden",
"rate_limited",
"network_error",
"unknown"
],
"description": "Machine-readable error code when unhealthy"
}
},
"required": [
"id",
"network",
"healthy",
"checkedAt"
],
"description": "Health status of the social account"
}
},
"required": [
"success",
"data"
],
"description": "Account health response"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts/{id}/health \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}/health', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Claude Code Plugin (https://www.outstand.so/docs/claude-code)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
Outstand publishes an official [Claude Code](https://claude.com/claude-code) plugin marketplace. The `outstand` plugin turns Claude Code into an
assistant that knows this API: it scaffolds a working integration into your codebase, diagnoses failures against your real account state, and
audits integrations you already have. It also bundles the [Outstand MCP server](/mcp), so the assistant can read your actual connected accounts
and posts instead of reasoning about your integration abstractly.
The marketplace is open source (MIT) at [github.com/Outstand-so/claude-code-marketplace](https://github.com/Outstand-so/claude-code-marketplace).
## Install
Run these two commands inside Claude Code:
```
/plugin marketplace add Outstand-so/claude-code-marketplace
/plugin install outstand@outstand
```
The first command registers the marketplace, the second installs the `outstand` plugin from it. Run `/plugin` afterwards to confirm the plugin is
listed and enabled.
To develop against a local clone of the marketplace repo, point the first command at a path instead:
```
/plugin marketplace add ./
```
### Prerequisites
1. **Claude Code** with plugin support (`/plugin` is available in your session).
2. **An Outstand account** for the bundled MCP server - [sign up free](https://www.outstand.so/app/signup). The commands work without it, but the
assistant can only reason about your code, not your live account state.
## What you get
| Command | Use it for |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `/outstand:start` | You are not sure where to begin. Triages your situation and routes you to the right flow. |
| `/outstand:integrate` | Scaffold a new integration, or add a capability to one you already have. |
| `/outstand:debug` | Diagnose an error, an unexpected response, or a post that did not publish where you expected. |
| `/outstand:review` | Audit an existing integration for the mistakes this API's async, partial-success shape invites. |
### `/outstand:start`
The entry point when you do not know which of the others you want. It detects your project (package manager, framework, existing Outstand code),
asks a short question or two, then hands off to the right flow. It is a router, not an interrogation.
```
/outstand:start
```
### `/outstand:integrate`
Scopes and scaffolds an integration: API client, account connection, post creation, scheduling, media upload, webhook handling, and database
schema - limited to what you actually asked for rather than every capability the API offers.
```
/outstand:integrate publish and schedule posts to LinkedIn and X, with webhooks
```
The flow is deliberately gated:
1. It detects what it can answer from your repo (and from your connected MCP session) before asking anything, then interviews you for the gaps in
at most two short rounds.
2. It confirms a written plan - platforms, webhooks vs polling, the files it will create or modify - and waits for your go-ahead. No files are
written before you approve.
3. It generates code. TypeScript/Node projects start from ready-made templates (API client, webhook handler, Prisma schema, smoke test) adapted to
your framework. Other stacks are generated fresh against the same contract, following your repo's existing HTTP client, ORM, and route
conventions.
4. It offers to run the smoke test against a real account so the integration is demonstrated working, not just claimed to be.
Whatever the stack, generated code is held to the same rules: treat `POST /v1/posts` as accepted rather than published, track status
[per account](/post-lifecycle) instead of with a single boolean, never retry `402`, honour `Retry-After` on `429`, complete the full three-step
[media upload](/get-upload-url), and pass an [`Idempotency-Key`](/idempotency) on post-creation paths.
React apps are steered towards [`@outstand-so/ui`](/sdk/ui) rather than hand-rolled OAuth redirect UI, with a warning that passing a raw `apiKey`
prop ships your key to the browser.
### `/outstand:debug`
Diagnoses a specific symptom: an error response, a post that reported success but never appeared on a platform, a webhook that is not firing, a
`402` or `429` you do not understand, or a scheduled post that appears stuck.
```
/outstand:debug my post says published but nothing showed up on Instagram
```
It asks for the real evidence - the actual HTTP status and body, or the actual webhook payload, not a paraphrase - before proposing a cause. If the
MCP server is connected it verifies against your live state using **read-only tools only** (`get_post`, `list_posts`, `get_social_account`,
`get_account_usage`). It never calls a tool that creates, updates, or deletes anything during diagnosis unless you explicitly ask for that action.
### `/outstand:review`
Dispatches a dedicated `outstand-reviewer` agent to audit your existing integration. The agent runs with write tools disabled, so it can only
report.
```
/outstand:review src/integrations/social
```
It checks for the failure modes that are specific to this API's architecture, ranked by severity:
| Severity | Checks |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| High | Post outcome treated as a single boolean instead of per-account status, API key exposed in client-side code, retries that re-create posts and double-post to platforms that already succeeded, `402` retried as if transient |
| Medium | No webhook handler (or tight polling in its place), missing idempotency on post creation, Outstand IDs not persisted or persisted without indexes |
| Low | Media URLs assumed permanent despite the retention window, `scheduledAt` timezone and exact-second assumptions, platform-specific gaps such as Pinterest pins created without a board |
You get a ranked findings list with file and line references first. Nothing is rewritten until you pick what to fix.
## You do not have to use the slash commands
Describing what you need in plain language routes to the same skills. "Help me post to LinkedIn and X from my Express app" reaches
`/outstand:integrate`; "why is my Instagram post stuck?" reaches `/outstand:debug`. The plugin ships five skills:
| Skill | Triggers on |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `outstand-docs` | Any authoritative fact about the API - a schema, a field name, an enum value, a status code. Every other skill depends on it. |
| `outstand-integration` | Adding Outstand to a codebase, fresh or incremental. |
| `outstand-troubleshooting` | Something broken, unexpected, or confusing in an existing integration. |
| `outstand-review` | Auditing code that already calls the API. |
| `outstand-platforms` | A named platform's connection method, configuration requirements, or quirks. |
## The bundled MCP server
The plugin ships the Outstand MCP server (`https://mcp.outstand.so/mcp`) as part of its configuration - there is nothing extra to add. The first
time a command needs it, Claude Code offers a one-click OAuth connection: sign in, pick the organization it may act on, and you are done. No API
key to paste.
For headless or CI use where a browser OAuth flow is not available, connect with an API key passed as a `Bearer` token instead. See the [MCP
setup guide](/mcp/setup) for that method, and the [tool reference](/mcp/tools) for what the server exposes.
Connecting it is optional but changes the quality of the help significantly: with it, `/outstand:debug` reads the actual post record that failed
and `/outstand:integrate` knows which platforms you have already connected.
## How it stays current
The plugin deliberately does **not** vendor a snapshot of these docs. Every skill fetches the live page at request time: it pulls
`https://www.outstand.so/docs/llms.txt` as an index, then the specific page as raw markdown by appending `.mdx` to its URL (for example
`https://www.outstand.so/docs/create-a-post.mdx`).
This is why answers cite the doc page they came from. A field name recalled from training data, or read out of a copy that shipped with a plugin
release six weeks ago, is exactly how an integration silently breaks. If a page cannot answer the question, the assistant is instructed to say so
rather than fill the gap from memory.
## Troubleshooting
**The marketplace is not found.** Check the owner spelling: the repository is `Outstand-so/claude-code-marketplace`, with the `-so` suffix.
**The Outstand tools are not available.** Run `/mcp` inside Claude Code to check the connection status and re-run the OAuth flow if needed. If you
are using an API key, confirm it starts with `ost_` and is still active in your [dashboard](https://www.outstand.so/app/).
**Removing the plugin.**
```
/plugin uninstall outstand@outstand
/plugin marketplace remove outstand
```
## Next steps
* [Getting started](/getting-started) - create an account, get an API key, make your first call
* [Backend integration](/backend-integration) - the full integration walkthrough the plugin scaffolds
* [MCP server](/mcp) - what the bundled server exposes
* [Webhooks](/webhooks) - the events the generated webhook handler consumes
# Confirm upload (https://www.outstand.so/docs/confirm-upload)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Confirm that a media file has been successfully uploaded. This marks the file as active and returns the public URL that can be used in posts.
**Important:** Call this endpoint only after successfully uploading the file to the presigned URL. The system will verify the file exists in storage before confirming.
## API Endpoint
`POST /v1/media/{id}/confirm`
**Summary:** Confirm upload
Confirm that a media file has been successfully uploaded. This marks the file as active and returns the public URL that can be used in posts.
**Important:** Call this endpoint only after successfully uploading the file to the presigned URL. The system will verify the file exists in storage before confirming.
**Tags:** Media
## Parameters
- **id** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"size": {
"type": "integer",
"exclusiveMinimum": 0,
"description": "The file size in bytes. If provided, it will be stored for reference.",
"example": 1024000
}
},
"description": "Request schema for confirming a media file upload"
}
```
## Responses
### 200
Upload confirmed successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the media file",
"example": "9dyJS"
},
"filename": {
"type": "string",
"description": "Original filename of the media file",
"example": "product-image.jpg"
},
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible URL for the media file",
"example": "https://media.outstand.so/org_abc123/550e8400-e29b-41d4-a716-446655440000/product-image.jpg"
},
"content_type": {
"type": [
"string",
"null"
],
"description": "MIME type of the media file",
"example": "image/jpeg"
},
"size": {
"type": [
"number",
"null"
],
"description": "File size in bytes",
"example": 1024000
},
"status": {
"type": "string",
"enum": [
"pending",
"active",
"deleted"
],
"description": "Current status of the media file",
"example": "active"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file was uploaded",
"example": "2025-01-15T10:30:00Z"
},
"expires_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file will expire (60 days from creation)",
"example": "2025-03-16T10:30:00Z"
}
},
"required": [
"id",
"filename",
"url",
"content_type",
"size",
"status",
"created_at",
"expires_at"
],
"description": "Media file object"
}
},
"required": [
"success",
"data"
],
"description": "Response after confirming media file upload"
}
```
### 400
Invalid request or file not found in storage
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 403
Unauthorized - media file belongs to different organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Media file not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Failed to generate upload URL"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/media/{id}/confirm \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"size": 1024000
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/media/{id}/confirm', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"size": 1024000
})
});
const data = await response.json();
```
# Connect a Bluesky account (https://www.outstand.so/docs/connect-a-bluesky-account)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Connect a Bluesky account using handle and app password authentication. Bluesky uses app passwords instead of OAuth, so credentials are provided directly.
**Prerequisites:**
1. Configure Bluesky in your social networks: `POST /v1/social-networks` with `network: "bluesky"`
2. Create an app password at [https://bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords)
**Security Note:** App passwords are scoped credentials that don't grant access to your main password. They can be revoked at any time from Bluesky settings.
## API Endpoint
`POST /v1/social-accounts/bluesky`
**Summary:** Connect a Bluesky account
Connect a Bluesky account using handle and app password authentication. Bluesky uses app passwords instead of OAuth, so credentials are provided directly.
**Prerequisites:**
1. Configure Bluesky in your social networks: `POST /v1/social-networks` with `network: "bluesky"`
2. Create an app password at https://bsky.app/settings/app-passwords
**Reconnecting:** if the handle is already connected for the same `tenantId`, the existing account is replaced (credentials and profile refreshed) instead of duplicated. Connecting the same handle under a different `tenantId` creates a separate account.
**Security Note:** App passwords are scoped credentials that don't grant access to your main password. They can be revoked at any time from Bluesky settings.
**Tags:** Social Accounts
## Request Body
```json
{
"type": "object",
"properties": {
"handle": {
"type": "string",
"minLength": 1,
"description": "Bluesky handle (e.g., user.bsky.social)",
"example": "user.bsky.social"
},
"appPassword": {
"type": "string",
"minLength": 1,
"description": "Bluesky app password (from bsky.app/settings/app-passwords)",
"example": "xxxx-xxxx-xxxx-xxxx"
},
"tenantId": {
"type": "string",
"description": "Optional tenant ID to associate with this account",
"example": "tenant_123"
}
},
"required": [
"handle",
"appPassword"
],
"description": "Request body for creating a Bluesky social account"
}
```
## Responses
### 201
Bluesky account connected successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the social account",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"description": "Organization ID that owns this social account",
"example": "org_abc123"
},
"tenant_id": {
"type": [
"string",
"null"
],
"description": "Your own tenant identifier for this account, as supplied when it was connected. Null when the account was connected without one - such an account is not matched by the `tenantId` filter.",
"example": "tenant_123"
},
"nickname": {
"type": "string",
"description": "User-friendly nickname for the social account",
"example": "My Company Twitter"
},
"network": {
"type": "string",
"description": "Social network platform (e.g., 'x', 'linkedin', 'instagram')",
"example": "x"
},
"username": {
"type": "string",
"description": "Username or handle for the social account",
"example": "@mycompany"
},
"profile_picture_url": {
"type": [
"string",
"null"
],
"description": "URL to the profile picture for the social account",
"example": "https://example.com/profile.jpg"
},
"network_unique_id": {
"type": "string",
"description": "Unique identifier for the account on the social network platform",
"example": "123456789"
},
"network_webhook_reference_id": {
"type": [
"string",
"null"
],
"description": "The id this network uses to identify the account in webhook payloads, when it differs from network_unique_id. Null when the platform uses the same id for both. Used by: Instagram.",
"example": "17841469709963147"
},
"customer_social_network_id": {
"type": "number",
"description": "ID of the customer social network configuration used to connect this account",
"example": 5
},
"accountType": {
"type": "string",
"description": "Type of account: 'personal', 'organization', or 'page'",
"example": "organization"
},
"isActive": {
"type": [
"number",
"null"
],
"description": "Whether the account is active (1) or inactive (0)",
"example": 1
},
"createdAt": {
"type": [
"string",
"null"
],
"description": "ISO 8601 timestamp when the account was connected",
"example": "2025-01-15T10:30:00Z"
},
"tokens": {
"type": "object",
"properties": {
"access_token": {
"type": [
"string",
"null"
],
"description": "OAuth access token for the account. Only present when the request included includeTokens=true.",
"example": "ya29.a0Af..."
}
},
"required": [
"access_token"
],
"description": "Sensitive OAuth tokens. Only included when includeTokens=true is passed."
}
},
"required": [
"id",
"orgId",
"tenant_id",
"nickname",
"network",
"username",
"profile_picture_url",
"network_unique_id",
"customer_social_network_id",
"accountType",
"isActive",
"createdAt"
],
"description": "Social account information"
}
},
"required": [
"success",
"data"
],
"description": "Successful Bluesky account creation response"
}
```
### 400
Invalid credentials or missing fields
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 409
Account already connected
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Account already connected"
}
},
"required": [
"success",
"error"
],
"description": "Conflict error response when account already exists"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/social-accounts/bluesky \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"handle": "user.bsky.social",
"appPassword": "xxxx-xxxx-xxxx-xxxx",
"tenantId": "tenant_123"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/bluesky', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"handle": "user.bsky.social",
"appPassword": "xxxx-xxxx-xxxx-xxxx",
"tenantId": "tenant_123"
})
});
const data = await response.json();
```
# Connect a new social network (https://www.outstand.so/docs/connect-a-new-social-network)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Store OAuth configuration for a social network platform to enable account connection and content publishing. This endpoint implements the Bring Your Own Key (BYOK) model, allowing you to use your own OAuth applications for complete control over the authentication flow.
**Workflow:**
1. Obtain OAuth credentials from the social network's developer portal (see platform-specific guides)
2. Store the social network using this endpoint
3. Connect social accounts using the social accounts API
4. Create and schedule posts
**Security:** The client\_secret is encrypted at rest using industry-standard encryption and is never returned in API responses. Only the client\_key (public identifier) is included in subsequent GET requests.
**Important:** Before using this endpoint, ensure you have valid OAuth credentials from the target platform's developer portal. Each platform has different requirements, approval processes, and credential naming conventions. Check our [configuration documentation](https://www.outstand.so/docs/configurations) for platform-specific guides.
## API Endpoint
`POST /v1/social-networks`
**Summary:** Connect a new social network
Store OAuth configuration for a social network platform to enable account connection and content publishing. This endpoint implements the Bring Your Own Key (BYOK) model, allowing you to use your own OAuth applications for complete control over the authentication flow.
**Workflow:**
1. Obtain OAuth credentials from the social network's developer portal (see platform-specific guides)
2. Store the social network using this endpoint
3. Connect social accounts using the social accounts API
4. Create and schedule posts
**Security:** The client_secret is encrypted at rest using industry-standard encryption and is never returned in API responses. Only the client_key (public identifier) is included in subsequent GET requests.
**Important:** Before using this endpoint, ensure you have valid OAuth credentials from the target platform's developer portal. Each platform has different requirements, approval processes, and credential naming conventions. Check our [configuration documentation](https://www.outstand.so/docs/configurations) for platform-specific guides.
**Tags:** Customer Social Networks
## Request Body
```json
{
"type": "object",
"properties": {
"oauth_callback_url": {
"type": [
"string",
"null"
],
"maxLength": 2048,
"description": "YouTube BYOK only. Exact customer-hosted HTTPS callback registered with Google. Omit to preserve the default; PATCH null clears it. Requires exactly one YouTube credential configuration.",
"example": "https://auth.example.com/youtube/callback"
},
"network": {
"type": "string",
"enum": [
"threads",
"bluesky",
"x",
"linkedin",
"youtube",
"instagram",
"facebook",
"tiktok",
"pinterest",
"google_business",
"vimeo",
"reddit"
],
"description": "Social network platform identifier. Each platform requires specific OAuth credentials obtained from their respective developer portals. Refer to our configuration guides for detailed instructions on obtaining credentials for each platform.",
"example": "x"
},
"client_key": {
"type": "string",
"minLength": 1,
"description": "OAuth Client ID, API Key, or App ID from the social network's developer portal. This is the public identifier for your OAuth application. Different platforms use different terminology (e.g., X/Twitter uses \"Client ID\", Meta apps use \"App ID\", LinkedIn uses \"Client ID\"). Obtain this from the respective platform's developer console after creating an application.",
"example": "abc123xyz789"
},
"client_secret": {
"type": "string",
"minLength": 1,
"description": "OAuth Client Secret, API Secret, or App Secret from the social network's developer portal. This is a private credential that should be kept secure and never exposed in client-side code or logs. Different platforms use different terminology (e.g., \"Client Secret\", \"App Secret\", \"Consumer Secret\"). This value is encrypted at rest and never returned in subsequent API responses.",
"example": "secret_abc123xyz789_do_not_share"
}
},
"required": [
"network",
"client_key",
"client_secret"
],
"description": "Schema for creating a new customer social network. Before creating a social network, ensure you have obtained valid OAuth credentials from the respective social network's developer portal. Each platform has different requirements and approval processes. Once stored, these credentials enable you to connect social media accounts and publish content through our unified API. For detailed instructions on obtaining credentials for specific platforms, refer to our [configuration documentation](https://www.outstand.so/docs/configurations)."
}
```
## Responses
### 200
Social network created successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Social network created successfully"
},
"data": {
"type": "object",
"properties": {
"oauth_callback_url": {
"type": [
"string",
"null"
],
"maxLength": 2048,
"description": "YouTube BYOK only. Exact customer-hosted HTTPS callback registered with Google. Omit to preserve the default; PATCH null clears it. Requires exactly one YouTube credential configuration.",
"example": "https://auth.example.com/youtube/callback"
},
"id": {
"type": "string",
"description": "Unique identifier for the network",
"example": "abc123"
},
"network": {
"type": "string",
"enum": [
"threads",
"bluesky",
"x",
"linkedin",
"youtube",
"instagram",
"facebook",
"tiktok",
"pinterest",
"google_business",
"vimeo",
"reddit"
],
"description": "Social network platform identifier. Each platform requires specific OAuth credentials obtained from their respective developer portals. Refer to our configuration guides for detailed instructions on obtaining credentials for each platform.",
"example": "x"
},
"client_key": {
"type": "string",
"description": "Client key for the social network",
"example": "your_client_key_here"
},
"createdAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was created",
"example": "2025-01-15T10:30:00Z"
},
"updatedAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was last updated",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"oauth_callback_url",
"id",
"network",
"client_key",
"createdAt",
"updatedAt"
],
"description": "Customer social network response (client_secret is never included)"
}
},
"required": [
"success",
"message",
"data"
],
"description": "Successful creation response"
}
```
### 400
Invalid request payload or validation error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 409
Ambiguous YouTube credential configuration for a custom callback
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/social-networks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"oauth_callback_url": "https://auth.example.com/youtube/callback",
"network": "x",
"client_key": "abc123xyz789",
"client_secret": "secret_abc123xyz789_do_not_share"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-networks', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"oauth_callback_url": "https://auth.example.com/youtube/callback",
"network": "x",
"client_key": "abc123xyz789",
"client_secret": "secret_abc123xyz789_do_not_share"
})
});
const data = await response.json();
```
# Create a first comment (https://www.outstand.so/docs/create-a-first-comment)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
> Functionality relies on the [post creation endpoint](create-a-post).
Sometimes you want to create a post with a first comment automatically posted once your post is published. This is useful for example
to add external links without compromising your post's reach, or to create artificial "engagement" to get the post more visibility.
In order for this to work, you need to create the post with the `containers` field, and include the comment as the first container.
For example, this is a simple post without a comment:
```json
{
"content": "This is a comment",
....
}
```
Skip using the `content` field and use the `containers` field instead, for both the post and the comment:
```json
{
"containers": [
{
"content": "This is the post content",
},
{
"content": "This is a comment",
}
]
}
```
When the post is published, the comment will be automatically posted as a reply to the post.
Comments can include media attachments, just like posts.
```json
{
"containers": [
{
"content": "This is the post content",
},
{
"content": "This is a comment",
"media": [
{
"url": "https://example.com/media.jpg",
}
]
}
]
}
```
## Network support
First comments (and the [publish a comment](publish-a-comment) / [get post replies](get-post-repliescomments) endpoints) rely on each network exposing a comments API. They are **not** supported on every network:
* **TikTok** - not supported.
* **Pinterest** - not supported (Pinterest has no comments API).
* **Google Business Profile** - not supported.
* **YouTube** - not yet supported.
If you include a reply container for an unsupported network, the root post still publishes normally, but the reply container is not delivered to that network.
# Create a Pinterest board (https://www.outstand.so/docs/create-a-pinterest-board)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Create a new board on the connected Pinterest account. Privacy defaults to `PUBLIC` if omitted. The returned `id` can be used as `pinterestConfiguration.board_id` when publishing pins via `POST /v1/posts/`.
## API Endpoint
`POST /v1/pinterest/accounts/{id}/boards`
**Summary:** Create a Pinterest board
Create a new board on the connected Pinterest account. Privacy defaults to `PUBLIC` if omitted. The returned `id` can be used as `pinterestConfiguration.board_id` when publishing pins via `POST /v1/posts/`.
**Tags:** Pinterest
## Parameters
- **id** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Board name",
"example": "Travel inspiration"
},
"description": {
"type": "string",
"description": "Optional board description",
"example": "Places I want to visit"
},
"privacy": {
"type": "string",
"enum": [
"PUBLIC",
"PROTECTED",
"SECRET"
],
"description": "Board privacy. Defaults to PUBLIC",
"example": "PUBLIC"
}
},
"required": [
"name"
],
"description": "Create board request"
}
```
## Responses
### 200
Board created
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Pinterest board ID",
"example": "987654321098765432"
},
"name": {
"type": "string",
"description": "Board name",
"example": "Summer Outfits"
},
"description": {
"type": "string",
"description": "Board description",
"example": "My favourite summer looks"
},
"pin_count": {
"type": "number",
"description": "Number of pins on the board",
"example": 42
},
"privacy": {
"type": "string",
"enum": [
"PUBLIC",
"PROTECTED",
"SECRET"
],
"description": "Board privacy",
"example": "PUBLIC"
},
"owner": {
"type": "object",
"properties": {
"username": {
"type": "string",
"example": "myaccount"
}
},
"required": [
"username"
],
"description": "Board owner"
},
"created_at": {
"type": "string",
"description": "ISO 8601 creation timestamp"
}
},
"required": [
"id",
"name",
"privacy"
],
"description": "A Pinterest board"
}
},
"required": [
"success",
"data"
],
"description": "Created Pinterest board"
}
```
### 400
Invalid payload
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/pinterest/accounts/{id}/boards \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Travel inspiration",
"description": "Places I want to visit",
"privacy": "PUBLIC"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/pinterest/accounts/{id}/boards', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"name": "Travel inspiration",
"description": "Places I want to visit",
"privacy": "PUBLIC"
})
});
const data = await response.json();
```
# Create a post (https://www.outstand.so/docs/create-a-post)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Create a new post to publish or schedule across multiple social media platforms simultaneously. A post might mean something different depending on the context and the platform - for example, a single X post, a multi-thread X post, a LinkedIn post, or an Instagram Reel or Story. Posts use a container model where each container holds content for one segment (e.g., individual tweets in an X thread, or carousel items). Specify target platforms via the `accounts` array, referencing each connected account by its `id` from `GET /v1/social-accounts` (preferred) or its exact username - network names (e.g. `x`, `linkedin`) and nicknames are not identifiers and do not resolve. The request is rejected with 400 only when none of the supplied identifiers resolve, so an entry matching nothing alongside one that matches is dropped silently. Provide the post body either as a single `content` string or as a `containers` array of `{ content, media }` objects - one of the two is required. Attach media by adding `media` objects to a container, each `{ url, filename }` where `url` is the publicly accessible URL returned by the confirm-upload step (see Media API); media is not referenced by id. Include `scheduledAt` (ISO 8601 format, e.g. `2025-06-01T12:00:00Z`) to schedule the post for future publishing, or omit it to publish immediately. Platform-specific options (e.g., TikTok privacy settings, YouTube categories, Pinterest board IDs) are provided as top-level keys named after the network (`tiktok`, `youtube`, `instagram`, `threads`, `google_business`, `pinterest`, `linkedin`, `facebook`, `vimeo`, `reddit`) - not nested under any wrapper object. The response echoes these back nested under `networkOverrideConfiguration` (e.g. `tiktokConfiguration`), so the request and response shapes for platform config differ by design. Unrecognised top-level keys are ignored and reported in the response `warnings` array. Managed-key YouTube uploads are admitted at execution time from a shared paced daily pool; scheduling never reserves capacity. A denied YouTube destination fails independently and its existing error string (also in webhook results) contains a stable reason code (release\_pending, org\_limit\_reached, daily\_limit\_reached, provider\_quota\_exhausted, or quota\_unavailable) and an ISO 8601 retryAt when known. retryAt estimates earliest eligibility for a capacity release, organization-cap relaxation, or next Pacific quota day; it is not a reservation. Retry manually. If another destination already published, create a new YouTube-only post targeting the failed account because partially published posts cannot be edited or rescheduled. BYOK bypasses the shared pool; dispatched attempts are not refunded on failure.
## API Endpoint
`POST /v1/posts`
**Summary:** Create a post
Create a new post to publish or schedule across multiple social media platforms simultaneously. A post might mean something different depending on the context and the platform - for example, a single X post, a multi-thread X post, a LinkedIn post, or an Instagram Reel or Story. Posts use a container model where each container holds content for one segment (e.g., individual tweets in an X thread, or carousel items). Specify target platforms via the `accounts` array, referencing each connected account by its `id` from `GET /v1/social-accounts` (preferred) or its exact username - network names (e.g. `x`, `linkedin`) and nicknames are not identifiers and do not resolve. The request is rejected with 400 only when none of the supplied identifiers resolve, so an entry matching nothing alongside one that matches is dropped silently. Provide the post body either as a single `content` string or as a `containers` array of `{ content, media }` objects - one of the two is required. Attach media by adding `media` objects to a container, each `{ url, filename }` where `url` is the publicly accessible URL returned by the confirm-upload step (see Media API); media is not referenced by id. Include `scheduledAt` (ISO 8601 format, e.g. `2025-06-01T12:00:00Z`) to schedule the post for future publishing, or omit it to publish immediately. Platform-specific options (e.g., TikTok privacy settings, YouTube categories, Pinterest board IDs) are provided as top-level keys named after the network (`tiktok`, `youtube`, `instagram`, `threads`, `google_business`, `pinterest`, `linkedin`, `facebook`, `vimeo`, `reddit`, `x`, `bluesky`) - not nested under any wrapper object. Every one of those blocks also accepts `content`: text published on that network instead of the shared body, so one post can carry a professional LinkedIn version, a casual Instagram caption and a short X version while the top-level `content` remains the fallback for the rest. Only the root text is overridden - media, first comments and thread replies stay shared, and two accounts on the same network get the same override. The response echoes these back nested under `networkOverrideConfiguration` (e.g. `tiktokConfiguration`), so the request and response shapes for platform config differ by design. Unrecognised top-level keys are ignored and reported in the response `warnings` array. Threads limits each container to 500 characters, so a request targeting a Threads account - on its own or cross-posted alongside other networks - is rejected with 400 if any container is longer; when `threads.content` is set, that override is checked in place of the shared body, which is how a long LinkedIn post cross-posts to Threads. Managed-key YouTube uploads are admitted at execution time from a shared paced daily pool; scheduling never reserves capacity. A denied YouTube destination fails independently and its existing error string (also in webhook results) contains a stable reason code (release_pending, org_limit_reached, daily_limit_reached, provider_quota_exhausted, or quota_unavailable) and an ISO 8601 retryAt when known. retryAt estimates earliest eligibility for a capacity release, organization-cap relaxation, or next Pacific quota day; it is not a reservation. Retry manually. If another destination already published, create a new YouTube-only post targeting the failed account because partially published posts cannot be edited or rescheduled. BYOK bypasses the shared pool; dispatched attempts are not refunded on failure. Google Business EVENT and OFFER posts require an event title, startDate, startTime, endDate and endTime. CALL buttons omit callToAction.url; other action types require it. Google ignores callToAction on OFFER posts.
**Tags:** Posts
## Parameters
- **Idempotency-Key** (header: string): Optional client-generated key (a UUID v4 is ideal, max 255 printable ASCII characters) that makes this create safe to retry. Retrying with the same key and the same request body returns the original response together with an `Idempotency-Replayed: true` header, instead of creating a second post. Keys are scoped to your organization and to this endpoint, and are remembered for 24 hours.
## Request Body
```json
{
"type": "object",
"properties": {
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string",
"default": "",
"description": "The text content of this post container. Optional when the container carries media - omit it for a media-only post, and for Facebook/Instagram Stories, which are published without a caption. Can include platform-specific formatting (e.g., hashtags, mentions) but it's not guaranteed to be rendered correctly by all platforms. Threads rejects containers longer than 500 characters, so a post targeting a Threads account (including when cross-posting alongside other networks) is rejected with 400 if any container exceeds it.",
"example": "Check out our new product launch! #excited"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible HTTPS URL to the media file. Requirements: (1) Must be publicly accessible - Outstand downloads media server-side, (2) Must use HTTPS, (3) Must return valid Content-Type header, (4) Must be available at publish time - media is fetched when publishing, not when creating the post, (5) Must not require authentication. Supported formats vary by platform: Facebook/X/LinkedIn support JPEG, PNG, GIF; Instagram/Threads support JPEG, PNG; Videos (MP4, MOV) supported on most platforms.",
"example": "https://media.yoursite.com/images/launch.jpg"
},
"filename": {
"type": "string",
"minLength": 1,
"description": "Clean filename with extension (e.g., 'photo.jpg', 'video.mp4'). Do not include query parameters or URL fragments. The extension helps determine media type for platform-specific processing.",
"example": "launch-photo.jpg"
},
"altText": {
"type": "string",
"description": "Per-image alternative text for accessibility. Currently applied on Bluesky, which supports alt text per image (app.bsky.embed.images). Other platforms use their own network-specific alt-text configuration and ignore this field.",
"example": "A product photo showing the new dashboard on a laptop screen"
}
},
"required": [
"url",
"filename"
],
"description": "Media attachment for a post container. Outstand handles media processing per platform: Facebook media is downloaded and re-uploaded as binary data; Instagram/Threads URLs are passed to their container APIs; X media is downloaded and uploaded via their media API; LinkedIn URLs are passed directly; YouTube/TikTok videos are downloaded and uploaded via their respective upload APIs."
},
"default": [],
"description": "Array of media attachments for this container. Multiple media files can be attached per container.",
"example": []
}
},
"description": "A single content container within a post. Posts can have multiple containers. A container can have multiple media attachments. Each container that is not the root container, will be published as a reply to the root container."
},
"description": "Array of post containers. Each container represents a combo for the post content with its own text and media. Use this if you want to create a post with an automated reply to the root container. Otherwise, use the 'content' field.",
"example": [
{
"content": "Our newest feature is live! Check it out 🚀",
"media": [
{
"url": "https://example.com/feature.png",
"filename": "feature.png"
}
]
}
]
},
"content": {
"type": "string",
"description": "Simple text content for the post. Use this for single-container posts. Either 'content' or 'containers' must be provided. If you use 'containers', the first container will be the root container and the rest will be published as replies to the root container. Threads rejects content longer than 500 characters, so a post targeting a Threads account (including when cross-posting alongside other networks) is rejected with 400 if it exceeds that - unless `threads.content` supplies a Threads-only body, which is what gets checked instead. To publish different text on one network, put `content` inside that network's block (e.g. `linkedin.content`); this field is then the fallback for every network without one.",
"example": "Excited to announce our latest update!"
},
"accounts": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"description": "Array of social media account identifiers where this post will be published. Each entry is either the account's `id` as returned by GET /v1/social-accounts (preferred) or the account's exact username. Network names (e.g. 'x', 'linkedin') and nicknames are NOT identifiers and will not resolve. Account ids are opaque - pass back exactly what the API returned and never construct one. A username is matched by exact equality with no network or active-account filter, so a username shared across networks publishes to every account that carries it. The request is rejected with 400 only when NONE of the supplied identifiers resolve; if at least one resolves, the unresolved entries are silently ignored, so compare the returned `socialAccounts` against what you sent.",
"example": [
"Kx7vQ",
"Tm2bN"
]
},
"scheduledAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp for when the post should be automatically published. If not provided, the post will be published immediately (unless it's a draft). If a time in the past is provided, the post will be published immediately. The maximum time in the future is 30d - a later timestamp is rejected with 400 and no post is created.",
"example": "2026-02-20T14:00:00Z"
},
"threads": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Threads instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Threads accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. The 500-character Threads limit is checked against this override instead of the shared body, so a longer shared body can still cross-post to Threads.",
"example": "A more conversational take for Threads"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of valid ISO 3166-1 alpha-2 country codes that represents the countries where this media should be shown. If this parameter is passed in, the post will not be shown to Threads profiles in countries outside of this list.",
"example": [
"US",
"DK"
]
},
"reply_control": {
"type": "string",
"enum": [
"everyone",
"accounts_you_follow",
"mentioned_only"
],
"description": "Controls who can reply to this Threads post. Set at publish time only - it cannot be changed after the post goes live. Defaults to 'everyone'.",
"example": "accounts_you_follow"
}
},
"description": "Custom and override configuration for Threads content. Not to be confused with threaded content or anything other than the Threads By Instagram - the social network."
},
"instagram": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Instagram instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Instagram accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Takes precedence over the older 'caption' field.",
"example": "Casual Instagram caption"
},
"publishAsStory": {
"type": "boolean",
"description": "When true, publish as an Instagram Story instead of a feed post. Stories support a single image or video (no carousel), no caption, and expire after 24 hours. Recommended aspect ratio: 9:16.",
"example": true
},
"locationId": {
"type": "string",
"description": "The ID of the location to publish the post to.",
"example": "1234567890"
},
"reelThumbOffset": {
"type": "number",
"description": "The offset in milliseconds for the thumbnail of the Reel. Not setting this will use the default thumbnail offset for the Reel, which is 500msec. Ignored if reelCoverUrl is set.",
"example": 1000
},
"reelCoverUrl": {
"type": "string",
"format": "uri",
"description": "Public URL of a JPEG image to use as the cover image of the Reel. Reels only - not supported for images, carousels or Stories. Instagram fetches the image server-side, so it must be publicly reachable. Max 8MB, sRGB, 9:16 aspect ratio recommended. Takes precedence over reelThumbOffset.",
"example": "https://cdn.example.com/reel-cover.jpg"
},
"userTags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"x": {
"type": "number"
},
"y": {
"type": "number"
}
},
"required": [
"username",
"x",
"y"
]
},
"description": "Array of user tags to add to the post",
"example": [
{
"username": "@instagram",
"x": 0.5,
"y": 0.5
}
]
},
"altText": {
"type": "string",
"description": "The alt text for the post - overrides the alt text in the post content/first container.",
"example": "A picture of our latest product"
},
"caption": {
"type": "string",
"description": "The caption for the post - overrides the caption in the post content/first container.",
"example": "Check out our new product! #excited"
},
"collaborators": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 3,
"description": "Array of up to 3 public Instagram usernames to invite as collaborators. Collaborators receive an invite; once accepted, the post appears on both profiles. Only supported for feed posts (IMAGE) and Reels, not Stories.",
"example": [
"partner_account",
"brand_account"
]
},
"isAiGenerated": {
"type": "boolean",
"description": "Self-disclosure that the media is AI-generated, surfaced as Instagram's \"AI info\" label. Applies to feed posts, Reels, carousels and Stories. For carousels the flag is set on the carousel itself, not the individual items.",
"example": true
},
"trialReel": {
"type": "object",
"properties": {
"graduationStrategy": {
"type": "string",
"enum": [
"MANUAL",
"SS_PERFORMANCE"
],
"description": "MANUAL: the creator graduates the trial reel to followers manually in the Instagram app. SS_PERFORMANCE: Meta automatically graduates the trial reel if it performs well with non-followers.",
"example": "MANUAL"
}
},
"required": [
"graduationStrategy"
],
"description": "Publish this Reel as a Trial Reel, shared only to non-followers until graduated. Requires a single video published as a Reel - not supported for Stories, carousels, or images.",
"example": {
"graduationStrategy": "MANUAL"
}
}
},
"description": "Custom and override configuration for Instagram content.",
"example": {
"locationId": "1234567890",
"reelThumbOffset": 3240
}
},
"youtube": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on YouTube instead of the shared post body. Replaces the top-level 'content' (the first container's text) for YouTube accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Becomes the video description; 'title' still applies.",
"example": "A longer description for the YouTube video."
},
"isShort": {
"type": "boolean",
"description": "Whether the video is a YouTube Short.",
"example": true
},
"categoryId": {
"type": "string",
"description": "The ID of the category to publish the post to.",
"example": "1234567890"
},
"privacyStatus": {
"type": "string",
"enum": [
"public",
"private",
"unlisted"
],
"description": "The privacy status of the post.",
"example": "public"
},
"madeForKids": {
"type": "boolean",
"description": "Whether the video is made for kids.",
"example": true
},
"containsSyntheticMedia": {
"type": "boolean",
"description": "Self-disclosure that the video contains realistic altered or synthetic content, for example AI-generated video. Maps to YouTube's status.containsSyntheticMedia and surfaces as an altered-content label on the video. Omitted from the upload entirely when not set - Outstand never declares this on your behalf. Cannot be changed through Outstand once the video is published.",
"example": true
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "The tags for the post.",
"example": [
"tag1",
"tag2"
]
},
"title": {
"type": "string",
"description": "The title of the post.",
"example": "My new video"
},
"thumbnailUrl": {
"type": "string",
"format": "uri",
"description": "Public URL of a JPEG or PNG image to set as the video's custom thumbnail. Max 50MB; 16:9 recommended for videos (3840x2160, at least 640px wide) and 9:16 for Shorts. The image is downloaded and checked before the video is uploaded, so an unreachable, oversized or non-JPEG/PNG image fails the publish. Setting it on YouTube is best-effort: if YouTube rejects it - for example because the channel is not verified for custom thumbnails - the video is still published with an auto-generated thumbnail. YouTube may ignore custom thumbnails on Shorts.",
"example": "https://cdn.example.com/video-thumbnail.jpg"
}
},
"description": "Custom and override configuration for YouTube content."
},
"tiktok": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on TikTok instead of the shared post body. Replaces the top-level 'content' (the first container's text) for TikTok accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. For photo posts, 'title' and 'description' still take precedence when set.",
"example": "Hook-first caption for TikTok"
},
"postMode": {
"type": "string",
"enum": [
"DIRECT_POST",
"MEDIA_UPLOAD"
],
"description": "The posting mode for TikTok. Defaults to MEDIA_UPLOAD when omitted. MEDIA_UPLOAD delivers the media to the creator's TikTok inbox as a draft - they finish the caption, visibility and settings in the TikTok app and publish it themselves - and is not subject to TikTok's 24-hour cap on distinct publishing creators per app. DIRECT_POST publishes straight to the profile, requires privacyLevel, and does count against that cap (reached_active_user_cap).",
"example": "MEDIA_UPLOAD"
},
"privacyLevel": {
"type": "string",
"enum": [
"PUBLIC_TO_EVERYONE",
"MUTUAL_FOLLOW_FRIENDS",
"FOLLOWER_OF_CREATOR",
"SELF_ONLY"
],
"description": "The privacy level for the TikTok post. Required when postMode is DIRECT_POST - must come from the creator_info privacy_level_options. Ignored for MEDIA_UPLOAD, where the creator picks visibility in the TikTok app.",
"example": "PUBLIC_TO_EVERYONE"
},
"title": {
"type": "string",
"maxLength": 90,
"description": "Photo post title (the bold header), mapped to TikTok's post_info.title. Max 90 UTF-16 runes. Photo posts only. When omitted, the post content is used and truncated to 90 characters.",
"example": "Summer collection drop"
},
"description": {
"type": "string",
"maxLength": 4000,
"description": "Photo post description (the caption), mapped to TikTok's post_info.description. Max 4000 UTF-16 runes. Photo posts only. When omitted, the post content is used.",
"example": "Our new summer collection is here. Swipe through to see every look."
},
"disableComment": {
"type": "boolean",
"description": "Whether to disable comments on the TikTok post. Applies to both video and photo posts.",
"example": false
},
"disableDuet": {
"type": "boolean",
"description": "Whether to disable duets on the TikTok post. Video posts only - ignored for photo posts (TikTok does not support Duet on photos).",
"example": false
},
"disableStitch": {
"type": "boolean",
"description": "Whether to disable stitches on the TikTok post. Video posts only - ignored for photo posts (TikTok does not support Stitch on photos).",
"example": false
},
"videoCoverTimestampMs": {
"type": "integer",
"minimum": 0,
"description": "Timestamp in milliseconds of the video frame to use as the cover, mapped to TikTok's post_info.video_cover_timestamp_ms. DIRECT_POST video posts only - ignored for MEDIA_UPLOAD (the creator picks the cover in the TikTok app) and for photo posts. When omitted, TikTok picks the default cover.",
"example": 3500
},
"autoAddMusic": {
"type": "boolean",
"description": "Whether TikTok should automatically add recommended music to the post. Applies to photo posts only (ignored for video posts). Defaults to false when not set.",
"example": false
},
"brandContentToggle": {
"type": "boolean",
"description": "Branded content disclosure - marks the post as a paid partnership (TikTok UX Guideline Point 3).",
"example": false
},
"brandOrganicToggle": {
"type": "boolean",
"description": "Your brand disclosure - marks the post as promotional content for your own brand (TikTok UX Guideline Point 3).",
"example": false
},
"isAigc": {
"type": "boolean",
"description": "Whether the content is AI-generated.",
"example": false
}
},
"description": "Custom and override configuration for TikTok content."
},
"google_business": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Google Business instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Google Business accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks.",
"example": "Local-audience wording for Google Business."
},
"topicType": {
"type": "string",
"enum": [
"STANDARD",
"EVENT",
"OFFER"
],
"description": "The type of Google Business post. STANDARD is a regular post, EVENT includes event details, OFFER includes promotional offer details.",
"example": "STANDARD"
},
"callToAction": {
"type": "object",
"properties": {
"actionType": {
"type": "string",
"enum": [
"BOOK",
"ORDER",
"SHOP",
"LEARN_MORE",
"SIGN_UP",
"CALL"
],
"description": "The type of call-to-action button.",
"example": "LEARN_MORE"
},
"url": {
"type": "string",
"format": "uri",
"description": "Required for non-CALL actions. Omit for CALL; any supplied URL is ignored when publishing.",
"example": "https://example.com"
}
},
"required": [
"actionType"
],
"description": "Call-to-action button configuration. Google ignores this for OFFER posts."
},
"event": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The title of the event.",
"example": "Grand Opening"
},
"startDate": {
"type": "object",
"properties": {
"year": {
"type": "number"
},
"month": {
"type": "number"
},
"day": {
"type": "number"
}
},
"required": [
"year",
"month",
"day"
],
"description": "The start date of the event.",
"example": {
"year": 2026,
"month": 3,
"day": 15
}
},
"startTime": {
"type": "object",
"properties": {
"hours": {
"type": "number"
},
"minutes": {
"type": "number"
}
},
"required": [
"hours",
"minutes"
],
"description": "The start time of the event.",
"example": {
"hours": 9,
"minutes": 0
}
},
"endDate": {
"type": "object",
"properties": {
"year": {
"type": "number"
},
"month": {
"type": "number"
},
"day": {
"type": "number"
}
},
"required": [
"year",
"month",
"day"
],
"description": "The end date of the event.",
"example": {
"year": 2026,
"month": 3,
"day": 15
}
},
"endTime": {
"type": "object",
"properties": {
"hours": {
"type": "number"
},
"minutes": {
"type": "number"
}
},
"required": [
"hours",
"minutes"
],
"description": "The end time of the event.",
"example": {
"hours": 17,
"minutes": 0
}
}
},
"required": [
"title",
"startDate",
"startTime",
"endDate",
"endTime"
],
"description": "Event details with all four schedule fields. Required when topicType is EVENT or OFFER."
},
"offer": {
"type": "object",
"properties": {
"couponCode": {
"type": "string",
"description": "The coupon code for the offer.",
"example": "SAVE20"
},
"redeemOnlineUrl": {
"type": "string",
"format": "uri",
"description": "URL where the offer can be redeemed online.",
"example": "https://example.com/redeem"
},
"termsConditions": {
"type": "string",
"description": "Terms and conditions for the offer.",
"example": "Valid until March 31, 2026."
}
},
"description": "Offer details. Used when topicType is OFFER."
}
},
"description": "Custom and override configuration for Google Business Profile content."
},
"pinterest": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Pinterest instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Pinterest accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Becomes the pin description; 'title' still applies.",
"example": "Pin description written for Pinterest search."
},
"board_id": {
"type": "string",
"minLength": 1,
"description": "The ID of the Pinterest board to pin to. Required for Pinterest.",
"example": "123456789"
},
"cover_image_url": {
"type": "string",
"format": "uri",
"description": "Cover image URL for video pins. Pinterest requires a cover image for videos; if omitted, the first frame of the video is used automatically.",
"example": "https://example.com/cover.jpg"
},
"link": {
"type": "string",
"format": "uri",
"description": "Destination URL when the pin is clicked.",
"example": "https://example.com/product"
},
"alt_text": {
"type": "string",
"maxLength": 500,
"description": "Alt text for the pin image (max 500 chars).",
"example": "A red dress on a model"
},
"title": {
"type": "string",
"maxLength": 100,
"description": "Pin title (max 100 chars). Defaults to first 100 chars of content.",
"example": "New Summer Collection"
}
},
"required": [
"board_id"
],
"description": "Custom configuration for Pinterest content. board_id is required when publishing to Pinterest."
},
"vimeo": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Vimeo instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Vimeo accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Becomes the video description; 'title' and 'description' still take precedence when set.",
"example": "Description for the Vimeo upload."
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"privacyView": {
"type": "string",
"enum": [
"anybody",
"unlisted",
"nobody",
"password",
"contacts",
"users",
"disable"
]
},
"privacyEmbed": {
"type": "string",
"enum": [
"public",
"private",
"whitelist"
]
}
},
"description": "Vimeo-specific video configuration"
},
"linkedin": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on LinkedIn instead of the shared post body. Replaces the top-level 'content' (the first container's text) for LinkedIn accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Mentions are resolved against this text.",
"example": "A more professional, detailed version for LinkedIn."
},
"article": {
"type": "object",
"properties": {
"source": {
"type": "string",
"format": "uri",
"description": "The URL the article card links to. Required. LinkedIn uses this as the primary link for the card.",
"example": "https://example.com/blog/my-article"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 400,
"description": "Title shown on the article card (max 400 chars). Overrides LinkedIn's OG title scraping.",
"example": "How We Scaled to 1M Users"
},
"description": {
"type": "string",
"maxLength": 4000,
"description": "Short description shown beneath the title on the card (max 4000 chars).",
"example": "A deep dive into our infrastructure journey."
},
"thumbnailUrl": {
"type": "string",
"format": "uri",
"description": "Public image URL to use as the card thumbnail. We upload this to LinkedIn's Images API and use the resulting URN. LinkedIn does not accept raw URLs as thumbnails.",
"example": "https://example.com/og-image.png"
}
},
"required": [
"source"
],
"description": "Publish the post as a LinkedIn article link card instead of a plain feed post. The container content becomes the commentary above the card."
},
"mentions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"urn": {
"type": "string",
"pattern": "^urn:li:(organization|person):.+",
"description": "The URN of the LinkedIn entity to mention. Organizations use 'urn:li:organization:'; members use 'urn:li:person:'.",
"example": "urn:li:organization:1337"
},
"text": {
"type": "string",
"minLength": 1,
"description": "The entity's exact, case-sensitive registered name as it appears on LinkedIn (e.g. 'Outstand.so', not 'Outstand'). This string must also appear verbatim in the post content - the first occurrence is converted into a clickable mention. If the text does not match the entity's full registered name, the API returns an error rather than publishing a silently-broken tag.",
"example": "LinkedIn"
}
},
"required": [
"urn",
"text"
]
},
"description": "Tag LinkedIn members or organizations by mentioning them inline. For each mention, the matching text in the post content is rewritten into LinkedIn's annotation format. Organization text must be the entity's exact, case-sensitive full name - pass the wrong name and the API will reject the request before publishing.",
"example": [
{
"urn": "urn:li:organization:1337",
"text": "LinkedIn"
}
]
},
"caption": {
"type": "string",
"minLength": 1,
"maxLength": 400,
"description": "Caption shown directly beneath the media in the LinkedIn feed. When omitted no caption is shown - LinkedIn does not add one of its own. Only visible on single-image and single-video posts; multi-image posts accept it but LinkedIn does not render a per-image caption. Ignored for PDF/document posts, which always carry a title, and for article-card posts, which have no media.",
"example": "Behind the scenes at our Berlin office"
}
},
"description": "Custom configuration for LinkedIn content."
},
"facebook": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Facebook instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Facebook accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Stories reject any caption, so it cannot be combined with publishAsStory.",
"example": "Facebook-specific wording for the Page post."
},
"mentions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"pageId": {
"type": "string",
"minLength": 1,
"description": "The numeric Facebook Page ID to tag (e.g. '107934268209008'). The API verifies that this page exists and is accessible before publishing - an invalid or inaccessible ID will cause the post to fail with a clear error.",
"example": "123456789"
},
"text": {
"type": "string",
"minLength": 1,
"description": "The anchor text that marks where the @[pageId] tag is inserted. This string must appear verbatim in the content of a comment container (not the post body) - if it is missing from all comments, the API rejects the request rather than silently skipping the tag. Note: Meta may still not render the tag as a clickable link for pages you do not manage or if the app's 'Page Mentioning' feature has not been approved (a platform limitation that cannot be detected before publishing).",
"example": "Outstand"
}
},
"required": [
"pageId",
"text"
]
},
"description": "Tag other Facebook Pages by mentioning them inline in a comment. IMPORTANT: Meta's Graph API only supports @[pageId] page mentions in comments, not in the main post body. Mentions are applied to comment containers (containers[1+]) - you must include at least one comment container whose text contains the anchor. Only Pages can be tagged; tagging personal profiles is not supported. Meta may silently suppress tags for pages your account has no relationship with.",
"example": [
{
"pageId": "123456789",
"text": "Outstand"
}
]
},
"publishAsStory": {
"type": "boolean",
"description": "When true, publish to the Page's Story instead of the feed. Stories take exactly one image or video (no carousel), no caption, no comments and no mentions - the request is rejected rather than silently dropping them, so a Story cannot be cross-posted in the same call as captioned content for other networks. Stories expire after 24 hours, after which analytics return null. Recommended aspect ratio: 9:16 (1080x1920).",
"example": true
},
"publishAsReel": {
"type": "boolean",
"description": "When true, publish the video as a Page Reel instead of a plain Page video. Requires exactly one video; the post content is used as the Reel description and comment containers are published as comments. Recommended aspect ratio: 9:16 (1080x1920), 3-90 seconds. Cannot be combined with publishAsStory.",
"example": true
}
},
"description": "Custom configuration for Facebook content."
},
"reddit": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Reddit instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Reddit accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Ignored for link posts (when 'url' is set).",
"example": "Markdown body written for the subreddit."
},
"subreddit": {
"type": "string",
"minLength": 1,
"description": "The subreddit to post to, without the 'r/' prefix (e.g. 'programming'). Required for Reddit. Use 'u_' to post to a user profile.",
"example": "programming"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 300,
"description": "The post title (max 300 chars). Required for Reddit - every Reddit post has a title separate from its body.",
"example": "We built a unified social media API"
},
"url": {
"type": "string",
"format": "uri",
"description": "When set, the post is published as a link post pointing at this URL and the container content is ignored (Reddit does not allow a body on link posts). When omitted, the post is a text (self) post with the container content as its markdown body.",
"example": "https://example.com/blog/launch"
},
"flairId": {
"type": "string",
"description": "Flair template ID to apply to the post. Some subreddits require flair and auto-remove posts without it.",
"example": "c4acd8f2-9e2f-11eb-8000-0e3e6a4e8e8a"
},
"flairText": {
"type": "string",
"maxLength": 64,
"description": "Custom flair text (max 64 chars), used together with flairId when the flair template allows editable text.",
"example": "Discussion"
},
"nsfw": {
"type": "boolean",
"description": "Mark the post as NSFW. Defaults to false.",
"example": false
},
"spoiler": {
"type": "boolean",
"description": "Mark the post as a spoiler. Defaults to false.",
"example": false
},
"sendReplies": {
"type": "boolean",
"description": "Whether the author receives inbox notifications for replies. Defaults to true.",
"example": true
}
},
"required": [
"subreddit",
"title"
],
"description": "Custom configuration for Reddit content. subreddit and title are required when publishing to Reddit. Only text (self) and link posts are supported - media containers are rejected."
},
"x": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on X instead of the shared post body. Replaces the top-level 'content' (the first container's text) for X accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. X's own length limit is not checked at request time.",
"example": "Short, punchy version for X"
}
},
"description": "Custom configuration for X content. Currently only the per-network content override."
},
"bluesky": {
"type": "object",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"description": "Text to publish on Bluesky instead of the shared post body. Replaces the top-level 'content' (the first container's text) for Bluesky accounts only - every other network still publishes the shared text, and the top-level 'content' acts as the fallback. Media, first comments and thread replies are not overridden; they stay shared across all networks. Bluesky's 300-character limit is not checked at request time.",
"example": "A version for Bluesky, within 300 characters"
}
},
"description": "Custom configuration for Bluesky content. Currently only the per-network content override."
},
"processMedia": {
"type": "boolean",
"description": "Opt in to automatic video processing. When true, video media is inspected and re-encoded to each target network's requirements (container, codec, resolution, frame rate and file size) before publishing, and the correct rendition is sent to each network. Leave unset to publish the file exactly as supplied. Billed per rendition produced, by output duration in 10-second blocks (rounded up, minimum one block per rendition); validation and inspection are always free. Has no effect on posts without video media.",
"example": true
}
},
"required": [
"accounts"
],
"description": "Schema for creating a new Post. A 'post' might mean something different depending on the context. For example, a 'post' might be a single tweet, a single LinkedIn post, a single Instagram Reel or Story, etc."
}
```
## Responses
### 200
Post created successfully and scheduled for publishing. Carries `Idempotency-Replayed: true` when this is a replay of an earlier request with the same `Idempotency-Key`.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Whether the post creation was successful",
"example": true
},
"warnings": {
"type": "array",
"items": {
"type": "string"
},
"description": "Non-fatal warnings about the request. Used to report keys in the request body that were not recognised and were therefore ignored - both top-level keys (e.g. the deprecated 'socialAccountIds'/'mediaIds' shapes, or the response-only 'networkOverrideConfiguration' wrapper) and keys inside a platform-config block (e.g. 'tiktok': { 'post_mode': ... } when the field is named 'postMode'). Unrecognised keys are dropped before the post is stored, so a warning here means the setting you sent is not in effect. Present only when there is at least one warning.",
"example": [
"Ignored unrecognised key 'post_mode' in 'tiktok'. Did you mean 'postMode'?"
]
},
"post": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the created post (sqids encoded)",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"description": "Organization ID that owns this post",
"example": "abc123"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the post was published, null if not yet published",
"example": null
},
"scheduledAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the post is scheduled to be published. If empty, the post is immediately published.",
"example": "2025-09-20T14:00:00Z"
},
"isDraft": {
"type": "boolean",
"description": "Whether this is a draft post",
"example": false
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the post was created",
"example": "2025-01-15T10:30:00Z"
},
"socialAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The account's encoded id - the same value GET /v1/social-accounts returns and the same value you put in the request's `accounts` array. This array lists exactly the accounts that resolved, so comparing these ids against the ids you sent is how you detect entries that were silently ignored. Note this is targeting, not publishing: per-account publish status arrives on GET /v1/posts/:id, not here.",
"example": "Kx7vQ"
},
"nickname": {
"type": "string",
"description": "User-defined nickname for this social account",
"example": "My Company"
},
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'facebook', 'x', 'instagram', 'linkedin', 'threads', 'youtube', 'tiktok')",
"example": "x"
},
"username": {
"type": "string",
"description": "Username or handle on the social network",
"example": "mycompany"
}
},
"required": [
"id",
"nickname",
"network",
"username"
],
"description": "Social account associated with the post"
},
"description": "Array of social accounts where this post will be published",
"example": []
},
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the container (sqids encoded)",
"example": "8xKmL"
},
"content": {
"type": "string",
"description": "The text content of this post container",
"example": "Check out our new product launch! #excited"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Unique identifier for the media file",
"example": 123
},
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible HTTPS URL to the media file",
"example": "https://media.yoursite.com/images/launch.jpg"
},
"filename": {
"type": "string",
"minLength": 1,
"description": "Clean filename with extension",
"example": "launch-photo.jpg"
}
},
"required": [
"id",
"url",
"filename"
],
"description": "Media attachment in the response"
},
"description": "Array of media attachments for this container",
"example": []
}
},
"required": [
"id",
"content",
"media"
],
"description": "A post container in the response"
},
"description": "Array of post containers with content and media",
"example": []
},
"networkOverrideConfiguration": {
"type": [
"object",
"null"
],
"additionalProperties": {},
"description": "The stored platform-specific configuration, keyed by network (e.g. 'tiktokConfiguration', 'youtubeConfiguration'). Echoed back from what was persisted - note the request accepts these as top-level keys named after the network ('tiktok', 'youtube', ...), so the request and response shapes differ. Null when no platform config was provided.",
"example": null
},
"scheduled": {
"type": "boolean",
"description": "Whether a publishing job is actually queued for this post. This is the field to check to confirm a post will publish - `true` means the job exists, `false` means nothing will publish it until it is rescheduled (PATCH a `scheduledAt`). Drafts are never queued, so they report `false` by design. Prefer this over inspecting internal fields.",
"example": true
}
},
"required": [
"id",
"orgId",
"publishedAt",
"scheduledAt",
"isDraft",
"createdAt",
"socialAccounts",
"containers",
"scheduled"
],
"description": "The post object",
"example": {
"id": "9dyJS",
"orgId": "abc123",
"publishedAt": null,
"scheduledAt": "2025-09-20T14:00:00Z",
"isDraft": false,
"createdAt": "2025-01-15T10:30:00Z",
"scheduled": true,
"socialAccounts": [
{
"id": "Kx7vQ",
"nickname": "My Company",
"network": "x",
"username": "mycompany"
}
],
"containers": [
{
"id": "8xKmL",
"content": "Check out our new product launch! #excited",
"media": [
{
"id": 123,
"url": "https://media.yoursite.com/images/launch.jpg",
"filename": "launch-photo.jpg"
}
]
}
]
}
}
},
"required": [
"success",
"post"
],
"description": "Successful post creation response"
}
```
### 202
The post was created but could not be queued for publishing (a transient Cloud Tasks failure). The post is returned with `scheduled: false` and nothing will publish it until you requeue it by PATCHing `scheduledAt`. Not returned for an out-of-window `scheduledAt` - that is rejected with 400 before the post is created.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Whether the post creation was successful",
"example": true
},
"warnings": {
"type": "array",
"items": {
"type": "string"
},
"description": "Non-fatal warnings about the request. Used to report keys in the request body that were not recognised and were therefore ignored - both top-level keys (e.g. the deprecated 'socialAccountIds'/'mediaIds' shapes, or the response-only 'networkOverrideConfiguration' wrapper) and keys inside a platform-config block (e.g. 'tiktok': { 'post_mode': ... } when the field is named 'postMode'). Unrecognised keys are dropped before the post is stored, so a warning here means the setting you sent is not in effect. Present only when there is at least one warning.",
"example": [
"Ignored unrecognised key 'post_mode' in 'tiktok'. Did you mean 'postMode'?"
]
},
"post": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the created post (sqids encoded)",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"description": "Organization ID that owns this post",
"example": "abc123"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the post was published, null if not yet published",
"example": null
},
"scheduledAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "When the post is scheduled to be published. If empty, the post is immediately published.",
"example": "2025-09-20T14:00:00Z"
},
"isDraft": {
"type": "boolean",
"description": "Whether this is a draft post",
"example": false
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the post was created",
"example": "2025-01-15T10:30:00Z"
},
"socialAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The account's encoded id - the same value GET /v1/social-accounts returns and the same value you put in the request's `accounts` array. This array lists exactly the accounts that resolved, so comparing these ids against the ids you sent is how you detect entries that were silently ignored. Note this is targeting, not publishing: per-account publish status arrives on GET /v1/posts/:id, not here.",
"example": "Kx7vQ"
},
"nickname": {
"type": "string",
"description": "User-defined nickname for this social account",
"example": "My Company"
},
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'facebook', 'x', 'instagram', 'linkedin', 'threads', 'youtube', 'tiktok')",
"example": "x"
},
"username": {
"type": "string",
"description": "Username or handle on the social network",
"example": "mycompany"
}
},
"required": [
"id",
"nickname",
"network",
"username"
],
"description": "Social account associated with the post"
},
"description": "Array of social accounts where this post will be published",
"example": []
},
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the container (sqids encoded)",
"example": "8xKmL"
},
"content": {
"type": "string",
"description": "The text content of this post container",
"example": "Check out our new product launch! #excited"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "Unique identifier for the media file",
"example": 123
},
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible HTTPS URL to the media file",
"example": "https://media.yoursite.com/images/launch.jpg"
},
"filename": {
"type": "string",
"minLength": 1,
"description": "Clean filename with extension",
"example": "launch-photo.jpg"
}
},
"required": [
"id",
"url",
"filename"
],
"description": "Media attachment in the response"
},
"description": "Array of media attachments for this container",
"example": []
}
},
"required": [
"id",
"content",
"media"
],
"description": "A post container in the response"
},
"description": "Array of post containers with content and media",
"example": []
},
"networkOverrideConfiguration": {
"type": [
"object",
"null"
],
"additionalProperties": {},
"description": "The stored platform-specific configuration, keyed by network (e.g. 'tiktokConfiguration', 'youtubeConfiguration'). Echoed back from what was persisted - note the request accepts these as top-level keys named after the network ('tiktok', 'youtube', ...), so the request and response shapes differ. Null when no platform config was provided.",
"example": null
},
"scheduled": {
"type": "boolean",
"description": "Whether a publishing job is actually queued for this post. This is the field to check to confirm a post will publish - `true` means the job exists, `false` means nothing will publish it until it is rescheduled (PATCH a `scheduledAt`). Drafts are never queued, so they report `false` by design. Prefer this over inspecting internal fields.",
"example": true
}
},
"required": [
"id",
"orgId",
"publishedAt",
"scheduledAt",
"isDraft",
"createdAt",
"socialAccounts",
"containers",
"scheduled"
],
"description": "The post object",
"example": {
"id": "9dyJS",
"orgId": "abc123",
"publishedAt": null,
"scheduledAt": "2025-09-20T14:00:00Z",
"isDraft": false,
"createdAt": "2025-01-15T10:30:00Z",
"scheduled": true,
"socialAccounts": [
{
"id": "Kx7vQ",
"nickname": "My Company",
"network": "x",
"username": "mycompany"
}
],
"containers": [
{
"id": "8xKmL",
"content": "Check out our new product launch! #excited",
"media": [
{
"id": 123,
"url": "https://media.yoursite.com/images/launch.jpg",
"filename": "launch-photo.jpg"
}
]
}
]
}
}
},
"required": [
"success",
"post"
],
"description": "Successful post creation response"
}
```
### 400
Invalid request payload or validation error - including a container that exceeds a targeted network's character limit (Threads: 500) - or a malformed `Idempotency-Key`
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 409
Another request with the same `Idempotency-Key` is still in progress. Retry after the interval given in the `Retry-After` header.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 422
The supplied `Idempotency-Key` was already used with a different request body. Use a new key.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accounts": [
"Kx7vQ",
"Tm2bN"
]
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"accounts": [
"Kx7vQ",
"Tm2bN"
]
})
});
const data = await response.json();
```
# Current usage (https://www.outstand.so/docs/current-usage)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieve comprehensive usage information for the current organization including social media account limits and post usage for the current billing period. This endpoint requires an active subscription and returns detailed information about account utilization.
## API Endpoint
`GET /v1/account/usage`
**Summary:** Current usage
Retrieve comprehensive usage information for the current organization. Returns the number of connected social accounts (current vs. limit), posts published in the current billing period, and billing period start/end dates. Use this endpoint to monitor your consumption, display usage dashboards, or enforce client-side limits before creating new posts or connecting new accounts. Requires an active subscription - returns 404 if no subscription is found.
**Tags:** Account, Usage
## Responses
### 200
Successfully retrieved usage information for the organization
```json
{
"type": "object",
"properties": {
"usage": {
"type": "object",
"properties": {
"socialAccounts": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"limit": {
"type": "number"
},
"remaining": {
"type": "number"
}
},
"required": [
"current",
"limit",
"remaining"
]
},
"posts": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"limit": {
"type": "number"
}
},
"required": [
"current"
]
},
"billingPeriod": {
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string"
}
},
"required": [
"start",
"end"
]
}
},
"required": [
"socialAccounts",
"posts",
"billingPeriod"
]
}
},
"required": [
"usage"
]
}
```
### 404
No active subscription found for the organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"error": {
"type": "string"
}
},
"required": [
"success",
"error"
]
}
```
### 500
Internal server error occurred while retrieving usage information
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"error": {
"type": "string"
}
},
"required": [
"success",
"error"
]
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/account/usage \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/account/usage', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Delete a comment (https://www.outstand.so/docs/delete-a-comment)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Delete a comment (that was created through Outstand) from the social network it lives on. Provide the post ID in the path and the comment's platform-specific ID as replyId. Use the platform\_post\_id or account\_username query param to select which connected account's credentials to use (optional when the post was published to only one account). Supported on Instagram and Threads. On Threads (requires the threads\_delete scope) only comments/replies created by the connected account can be deleted - other users' replies can be hidden instead (see the Threads API). Comment deletion is not supported on every network and will return an error where unavailable.
## API Endpoint
`DELETE /v1/posts/{id}/replies/{replyId}`
**Summary:** Delete a comment
Delete a comment (that was created through Outstand) from the social network it lives on. Provide the post ID in the path and the comment's platform-specific ID as replyId. Use the platform_post_id or account_username query param to select which connected account's credentials to use (optional when the post was published to only one account). Supported on Instagram and Threads. On Threads (requires the threads_delete scope) only comments/replies created by the connected account can be deleted - other users' replies can be hidden instead (see the Threads API). Comment deletion is not supported on every network and will return an error where unavailable.
**Tags:** Posts, Comments
## Parameters
- **platform_post_id** (query: string): The platform-specific post ID of the account whose credentials should be used to delete the comment. Optional when the post was published to only one account.
- **account_username** (query: string): The username or nickname of the connected account whose credentials should be used. Optional when the post was published to only one account.
- **id** (path: string) [required]
- **replyId** (path: string) [required]
## Responses
### 200
Comment deleted successfully.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if the comment was successfully deleted from the social network.",
"example": true
}
},
"required": [
"success"
]
}
```
### 400
Invalid request, or the target network does not support deleting comments via API.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post or matching social account not found.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/posts/{id}/replies/{replyId} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/replies/{replyId}', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Delete a post from social networks (https://www.outstand.so/docs/delete-a-post-from-social-networks)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Delete a published post from all remote social network platforms it was published to. The post record is kept in the Outstand database but marked as deleted. Returns per-account results - networks that do not support post deletion via API (TikTok, Instagram) will appear with status='failed'. Threads is supported (requires the threads\_delete scope) and only removes posts created by the connected account. At least one successful deletion returns HTTP 200 with success=true.
## API Endpoint
`DELETE /v1/posts/{id}/remote`
**Summary:** Delete a post from social networks
Delete a published post from all remote social network platforms it was published to. The post record is kept in the Outstand database but marked as deleted. Returns per-account results - networks that do not support post deletion via API (TikTok, Instagram) will appear with status='failed'. Threads is supported (requires the threads_delete scope) and only removes posts created by the connected account. At least one successful deletion returns HTTP 200 with success=true.
**Tags:** Posts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Remote deletion attempted. Check the 'results' array for per-account status. 'success' is true if at least one network deleted the post.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if at least one social network account had its post successfully deleted. False if all accounts failed.",
"example": true
},
"results": {
"type": "array",
"items": {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'x', 'linkedin', 'bluesky')",
"example": "x"
},
"username": {
"type": "string",
"description": "Username or handle on the social network",
"example": "mycompany"
},
"platform_post_id": {
"type": [
"string",
"null"
],
"description": "The platform-specific post ID that was targeted for deletion",
"example": "1234567890"
},
"status": {
"type": "string",
"enum": [
"deleted",
"failed"
],
"description": "'deleted': post was successfully removed from the platform and its status is marked deleted. 'failed': deletion failed or the network does not support deleting posts via API (see error field).",
"example": "deleted"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message when status is 'failed'. Examples: 'TikTok does not support deleting posts via API', 'X API error 403: Forbidden'.",
"example": null
}
},
"required": [
"network",
"username",
"platform_post_id",
"status",
"error"
]
},
"description": "Per-account deletion results. Networks without a delete API (TikTok, Instagram, Threads) will appear here with status='failed'."
}
},
"required": [
"success",
"results"
]
}
```
### 400
Post has not been published to any social network yet.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 403
Unauthorized - post does not belong to your organization.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/posts/{id}/remote \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/remote', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Delete a social account (https://www.outstand.so/docs/delete-a-social-account)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
**WARNING: This is an irrevocable action.**
Permanently deletes a social account and revokes all access. This action:
* **Cannot be undone** - the social account connection is permanently removed
* **Revokes access entirely** - OAuth tokens are deleted and platform access is terminated
* **Deletes all social account data immediately** - profile information, tokens, and metadata are removed from our system
* **Posts cleanup** - Posts made from this account will eventually be cleaned up from our system (the posts remain on the social platform)
To reconnect this account, you will need to complete the full OAuth flow again.
## API Endpoint
`DELETE /v1/social-accounts/{id}`
**Summary:** Delete a social account
**WARNING: This is an irrevocable action.**
Permanently deletes a social account and revokes all access. This action:
- **Cannot be undone** - the social account connection is permanently removed
- **Revokes access entirely** - OAuth tokens are deleted and platform access is terminated
- **Deletes all social account data immediately** - profile information, tokens, and metadata are removed from our system
- **Posts cleanup** - Posts made from this account will eventually be cleaned up from our system (the posts remain on the social platform)
To reconnect this account, you will need to complete the full OAuth flow again.
**Tags:** Social Accounts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Social account deleted successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Social account deleted successfully"
}
},
"required": [
"success",
"message"
],
"description": "Successful deletion response"
}
```
### 404
Social account not found or not owned by organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/social-accounts/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Delete & cancel a post (https://www.outstand.so/docs/delete-cancel-a-post)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Cancel a scheduled post by removing it from the publishing queue and deleting it from the database. If the post is scheduled (not yet published), the publishing task is cancelled first, then the post record is removed. If the post was already published, it is only deleted from the Outstand database - the published content remains on the social media platforms. This action cannot be undone.
## API Endpoint
`DELETE /v1/posts/{id}`
**Summary:** Delete & cancel a post
Cancel a scheduled post by removing it from the publishing queue and deleting it from the database. If the post is scheduled (not yet published), the publishing task is cancelled first, then the post record is removed. If the post was already published, it is only deleted from the Outstand database - the published content remains on the social media platforms. This action cannot be undone.
**Tags:** Posts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Post cancelled from publishing queue and deleted successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Whether the post cancellation was successful",
"example": true
},
"message": {
"type": "string",
"description": "Success message",
"example": "Post cancelled and deleted successfully"
}
},
"required": [
"success",
"message"
],
"description": "Successful post cancellation response"
}
```
### 403
Unauthorized - post does not belong to your organization. Activity will be recorded.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found. Potential reason: post was already deleted.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/posts/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Delete media file (https://www.outstand.so/docs/delete-media-file)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Permanently delete a media file from storage and mark it as deleted in the database. This action cannot be undone. The file will be removed from blob storage immediately. Any posts that reference this media ID will retain their existing published content, but the media file will no longer be accessible for new posts.
## API Endpoint
`DELETE /v1/media/{id}`
**Summary:** Delete media file
Permanently delete a media file from storage and mark it as deleted in the database. This action cannot be undone. The file will be removed from blob storage immediately. Any posts that reference this media ID will retain their existing published content, but the media file will no longer be accessible for new posts.
**Tags:** Media
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Media file deleted successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"description": "Success message",
"example": "Media file deleted successfully"
}
},
"required": [
"success",
"message"
],
"description": "Response after deleting a media file"
}
```
### 403
Unauthorized - media file belongs to different organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Media file not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Failed to generate upload URL"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/media/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/media/{id}', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Disconnect a social network (https://www.outstand.so/docs/disconnect-a-social-network)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Permanently delete a social network from your organization. This action cannot be undone.
**Impact:**
* The social network will be immediately removed from your account
* Any social accounts connected using this social network may lose functionality. Talk with our support if you need to migrate to a different key set and need assistance.
* You will no longer be able to connect new accounts for this platform
* Existing posts will not be affected, but future posts to this platform will fail. Pulling metrics will fail for all posts.
**Use Cases:**
* Removing support for a platform you no longer use
* Cleaning up test or invalid social networks
* Security response to compromised credentials
**Best Practice:** Before deleting a social network, ensure you have a replacement ready or that you no longer need to connect accounts for this platform.
## API Endpoint
`DELETE /v1/social-networks/{id}`
**Summary:** Disconnect a social network
Permanently delete a social network from your organization. This action cannot be undone.
**Impact:**
- The social network will be immediately removed from your account
- Any social accounts connected using this social network may lose functionality. Talk with our support if you need to migrate to a different key set and need assistance.
- You will no longer be able to connect new accounts for this platform
- Existing posts will not be affected, but future posts to this platform will fail. Pulling metrics will fail for all posts.
**Use Cases:**
- Removing support for a platform you no longer use
- Cleaning up test or invalid social networks
- Security response to compromised credentials
**Best Practice:** Before deleting a social network, ensure you have a replacement ready or that you no longer need to connect accounts for this platform.
**Tags:** Customer Social Networks
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Social Network deleted successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"message": {
"type": "string",
"example": "Social network deleted successfully"
}
},
"required": [
"success",
"message"
],
"description": "Successful deletion response"
}
```
### 403
Unauthorized - social network does not belong to your organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Social network not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X DELETE https://api.outstand.so/v1/social-networks/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-networks/{id}', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Finalize pending connection (https://www.outstand.so/docs/finalize-pending-connection)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Complete the OAuth connection by selecting which pages/accounts to connect. This endpoint creates the social account records for the selected pages.
**Authentication:** Requires your API key, *and* the session token must belong to the same organization as that key. A session token from another organization is rejected as if it had expired. Call this from your backend.
**Flow:**
1. Call GET /pending/:sessionToken to get available pages
2. User selects which pages to connect
3. Call this endpoint with the selected page IDs
4. Social accounts are created and returned
**Note:** The session is automatically deleted after successful finalization.
## API Endpoint
`POST /v1/social-accounts/pending/{sessionToken}/finalize`
**Summary:** Finalize pending connection
Complete the OAuth connection by selecting which pages/accounts to connect. This endpoint creates the social account records for the selected pages.
**Authentication:** Requires your API key, *and* the session token must belong to the same organization as that key. A session token from another organization is rejected as if it had expired. Call this from your backend.
**Flow:**
1. Call GET /pending/:sessionToken to get available pages
2. User selects which pages to connect
3. Call this endpoint with the selected page IDs
4. Social accounts are created and returned
**Note:** The session is automatically deleted after successful finalization.
**Tags:** Pending Connections
## Parameters
- **sessionToken** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"selectedPageIds": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"description": "Array of page IDs to connect",
"example": [
"abc123",
"org456"
]
}
},
"required": [
"selectedPageIds"
],
"description": "Request body for finalizing connection"
}
```
## Responses
### 200
Connection finalized successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"connectedAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the connected account",
"example": "9dyJS"
},
"nickname": {
"type": "string",
"description": "Nickname of the connected account",
"example": "johndoe"
},
"username": {
"type": "string",
"description": "Username of the connected account",
"example": "johndoe"
},
"network": {
"type": "string",
"description": "Social network type",
"example": "linkedin"
},
"accountType": {
"type": "string",
"description": "Type of account",
"example": "personal"
}
},
"required": [
"id",
"nickname",
"username",
"network",
"accountType"
],
"description": "Successfully connected account"
}
}
},
"required": [
"success",
"connectedAccounts"
],
"description": "Response after successfully finalizing connection"
}
```
### 400
Invalid request - missing or invalid parameters
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Session expired or invalid
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/social-accounts/pending/{sessionToken}/finalize \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"selectedPageIds": [
"abc123",
"org456"
]
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/pending/{sessionToken}/finalize', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"selectedPageIds": [
"abc123",
"org456"
]
})
});
const data = await response.json();
```
# Get account metrics (https://www.outstand.so/docs/get-account-metrics)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Fetch analytics and metrics for a connected social account. Returns follower counts, engagement metrics, and platform-specific statistics.
**Supported Metrics by Platform:**
* **Instagram:** followers, following, media count, engagement (views, likes, comments, shares, saves, reach, accounts\_engaged, total\_interactions) - requires Business/Creator account with `instagram_business_manage_insights` permission (supports date range)
* **Facebook:** page followers/fans, engagement (impressions, engaged\_users, post\_engagements, consumptions) - supports date range
* **Threads:** followers, engagement (views, likes, replies, reposts, quotes) - supports date range
* **LinkedIn:** followers (personal & organization), engagement for organizations (likes, comments, shares, clicks, impressions, engagement\_rate) - requires `rw_organization_admin` permission, supports date range
* **TikTok:** followers, following, total likes, video count - current snapshot only, no date range support
* **YouTube:** subscribers, total views, video count - lifetime statistics only, no date range support
* **X (Twitter):** followers, following, tweet count (current snapshot), plus engagement (views, likes, comments, shares, quotes) aggregated from up to 500 of the account's own tweets posted within the date range. Aggregation is best-effort and may be unavailable under X API rate limits or tier restrictions. **Cost note:** this reads posts from your connected X app and counts against your X API quota / pay-per-use cost (\~$0.005 per post read); a busy account can cost up to \~$2.50 per call. Date range supported.
* **BlueSky:** followers, following, posts count - current snapshot only (Bluesky exposes no time-series analytics, so the date range is ignored). No engagement metrics.
* **Pinterest:** followers, following, pin count (`posts_count` includes both created and saved pins, per Pinterest's API), engagement (views/impressions, saves, pin\_clicks, outbound\_clicks); full daily analytics available under `platform_specific`. Date range supported.
**Date Range:** Use `since` and `until` query parameters for platforms that support historical data. Defaults to last 30 days.
## API Endpoint
`GET /v1/social-accounts/{id}/metrics`
**Summary:** Get account metrics
Fetch analytics and metrics for a connected social account. Returns follower counts, engagement metrics, and platform-specific statistics.
**Supported Metrics by Platform:**
- **Instagram:** followers, following, media count, engagement (views, likes, comments, shares, saves, reach, accounts_engaged, total_interactions) - requires Business/Creator account with `instagram_business_manage_insights` permission (supports date range)
- **Facebook:** page followers/fans, engagement (impressions, reach, post_engagements) - requires `pages_read_engagement` and `read_insights`, supports date range. Since Meta's June 2026 deprecation, impressions and reach are derived from media views and read lower than the retired impression metrics; metrics Meta did not return are listed in `platform_specific.insights_unavailable`
- **Threads:** followers, engagement (views, likes, replies, reposts, quotes) - supports date range
- **LinkedIn:** followers (personal & organization), engagement for organizations (likes, comments, shares, clicks, impressions, engagement_rate) - requires `rw_organization_admin` permission, supports date range
- **TikTok:** followers, following, total likes, video count - current snapshot only, no date range support
- **YouTube:** subscribers, total views, video count - lifetime statistics only, no date range support
- **X (Twitter):** followers, following, tweet count (current snapshot), plus engagement (views, likes, comments, shares, quotes) aggregated from up to 500 of the account's own tweets posted within the date range. Aggregation is best-effort and may be unavailable under X API rate limits or tier restrictions. **Cost note:** this reads posts from your connected X app and counts against your X API quota / pay-per-use cost (~$0.005 per post read); a busy account can cost up to ~$2.50 per call. Date range supported.
- **BlueSky:** followers, following, posts count - current snapshot only (Bluesky exposes no time-series analytics, so the date range is ignored). No engagement metrics.
- **Pinterest:** followers, following, pin count (`posts_count` includes both created and saved pins, per Pinterest's API), engagement (views/impressions, saves, pin_clicks, outbound_clicks); full daily analytics available under `platform_specific`. Date range supported.
**Date Range:** Use `since` and `until` query parameters for platforms that support historical data. Defaults to last 30 days.
**Tags:** Social Accounts
## Parameters
- **since** (query: number): Unix timestamp for start of date range (defaults to 30 days ago)
- **until** (query: number): Unix timestamp for end of date range (defaults to now)
- **id** (path: string) [required]
## Responses
### 200
Account metrics retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"example": "9dyJS"
},
"network": {
"type": "string",
"example": "tiktok"
},
"followers_count": {
"type": [
"number",
"null"
],
"example": 10000
},
"following_count": {
"type": [
"number",
"null"
],
"example": 500
},
"posts_count": {
"type": [
"number",
"null"
],
"example": 150
},
"engagement": {
"type": [
"object",
"null"
],
"properties": {
"views": {
"type": [
"number",
"null"
],
"example": 15000
},
"likes": {
"type": [
"number",
"null"
],
"example": 500
},
"comments": {
"type": [
"number",
"null"
],
"example": 75
},
"shares": {
"type": [
"number",
"null"
],
"example": 25
},
"saves": {
"type": [
"number",
"null"
],
"example": 10
},
"reposts": {
"type": [
"number",
"null"
],
"example": 10
},
"quotes": {
"type": [
"number",
"null"
],
"example": 5
},
"reach": {
"type": [
"number",
"null"
],
"example": 5000
},
"accounts_engaged": {
"type": [
"number",
"null"
],
"example": 200
},
"total_interactions": {
"type": [
"number",
"null"
],
"example": 600
},
"replies": {
"type": [
"number",
"null"
],
"example": 12,
"description": "Instagram: account-level story replies"
},
"profile_links_taps": {
"type": [
"number",
"null"
],
"example": 30
}
},
"additionalProperties": true,
"description": "Engagement metrics for the date range. Fields vary by platform."
},
"stories": {
"type": [
"object",
"null"
],
"properties": {
"views": {
"type": [
"number",
"null"
]
},
"reach": {
"type": [
"number",
"null"
]
},
"total_interactions": {
"type": [
"number",
"null"
]
},
"replies": {
"type": [
"number",
"null"
]
}
},
"description": "Instagram: engagement attributable to Stories over the date range (media_product_type=STORY breakdown)"
},
"platform_specific": {
"type": "object",
"additionalProperties": {},
"description": "Raw platform-specific data from the API"
},
"period": {
"type": "object",
"properties": {
"since": {
"type": "number",
"example": 1704067200
},
"until": {
"type": "number",
"example": 1706745600
},
"note": {
"type": "string",
"example": "This platform returns current snapshot, not historical data"
}
},
"required": [
"since",
"until"
],
"description": "Date range for the metrics"
},
"fetched_at": {
"type": "string",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"account_id",
"network",
"followers_count",
"following_count",
"posts_count",
"engagement",
"platform_specific",
"period",
"fetched_at"
],
"description": "Account metrics data"
}
},
"required": [
"success",
"data"
],
"description": "Account metrics response"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts/{id}/metrics \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}/metrics', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get details of a social network (https://www.outstand.so/docs/get-details-of-a-social-network)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieve a single social network by its unique identifier. This endpoint is useful when you need to verify specific social network details or check the status for a particular platform.
**Security:** The client\_secret is never included in the response, even though it is stored in the database. Only the client\_key and metadata are returned.
**Authorization:** Only social networks belonging to your organization can be accessed. Attempts to access social networks from other organizations will return a 403 Forbidden error.
## API Endpoint
`GET /v1/social-networks/{id}`
**Summary:** Get details of a social network
Retrieve a single social network by its unique identifier. This endpoint is useful when you need to verify specific social network details or check the status for a particular platform.
**Security:** The client_secret is never included in the response, even though it is stored in the database. Only the client_key and metadata are returned.
**Authorization:** Only social networks belonging to your organization can be accessed. Attempts to access social networks from other organizations will return a 403 Forbidden error.
**Tags:** Customer Social Networks
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Social network retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"oauth_callback_url": {
"type": [
"string",
"null"
],
"maxLength": 2048,
"description": "YouTube BYOK only. Exact customer-hosted HTTPS callback registered with Google. Omit to preserve the default; PATCH null clears it. Requires exactly one YouTube credential configuration.",
"example": "https://auth.example.com/youtube/callback"
},
"id": {
"type": "string",
"description": "Unique identifier for the network",
"example": "abc123"
},
"network": {
"type": "string",
"enum": [
"threads",
"bluesky",
"x",
"linkedin",
"youtube",
"instagram",
"facebook",
"tiktok",
"pinterest",
"google_business",
"vimeo",
"reddit"
],
"description": "Social network platform identifier. Each platform requires specific OAuth credentials obtained from their respective developer portals. Refer to our configuration guides for detailed instructions on obtaining credentials for each platform.",
"example": "x"
},
"client_key": {
"type": "string",
"description": "Client key for the social network",
"example": "your_client_key_here"
},
"createdAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was created",
"example": "2025-01-15T10:30:00Z"
},
"updatedAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was last updated",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"oauth_callback_url",
"id",
"network",
"client_key",
"createdAt",
"updatedAt"
],
"description": "Customer social network response (client_secret is never included)"
}
},
"required": [
"success",
"data"
],
"description": "Successful operation response with network data"
}
```
### 403
Unauthorized - social network does not belong to your organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Social network not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-networks/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-networks/{id}', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get import job status (https://www.outstand.so/docs/get-import-job-status)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns the current status and progress of an import job.
## API Endpoint
`GET /v1/social-accounts/{id}/imports/{importId}`
**Summary:** Get import job status
Returns the current status and progress of an import job.
**Tags:** Social Accounts
## Parameters
- **id** (path: string) [required]
- **importId** (path: string) [required]
## Responses
### 200
Import job retrieved
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"orgId": {
"type": "string"
},
"socialAccountId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed",
"partial"
]
},
"since": {
"type": [
"string",
"null"
]
},
"until": {
"type": [
"string",
"null"
]
},
"limit": {
"type": [
"number",
"null"
]
},
"imported": {
"type": "number"
},
"skipped": {
"type": "number"
},
"failed": {
"type": "number"
},
"error": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"completedAt": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"orgId",
"socialAccountId",
"status",
"since",
"until",
"limit",
"imported",
"skipped",
"failed",
"error",
"createdAt",
"updatedAt",
"completedAt"
],
"description": "Import job status"
}
},
"required": [
"success",
"data"
]
}
```
### 404
Import job not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts/{id}/imports/{importId} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}/imports/{importId}', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get media file (https://www.outstand.so/docs/get-media-file)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Get details of a specific media file by its ID, including the public URL, content type, file size, upload status (pending or active), and expiration date. The media ID is the encoded string identifier returned during the upload flow.
## API Endpoint
`GET /v1/media/{id}`
**Summary:** Get media file
Get details of a specific media file by its ID, including the public URL, content type, file size, upload status (pending or active), and expiration date. The media ID is the encoded string identifier returned during the upload flow.
For videos, `video` carries the processing pipeline state: what ffprobe read off the file, and any normalized renditions with their public URLs. It is `null` for images and for videos that have never been probed. Per-network compliance is not reported here - what a network will accept only becomes answerable once a post says where the video is going, so look at `containers[].media[].processing.issues` on `GET /v1/posts/{id}` for that.
**Tags:** Media
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Media file retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the media file",
"example": "9dyJS"
},
"filename": {
"type": "string",
"description": "Original filename of the media file",
"example": "product-image.jpg"
},
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible URL for the media file",
"example": "https://media.outstand.so/org_abc123/550e8400-e29b-41d4-a716-446655440000/product-image.jpg"
},
"content_type": {
"type": [
"string",
"null"
],
"description": "MIME type of the media file",
"example": "image/jpeg"
},
"size": {
"type": [
"number",
"null"
],
"description": "File size in bytes",
"example": 1024000
},
"status": {
"type": "string",
"enum": [
"pending",
"active",
"deleted"
],
"description": "Current status of the media file",
"example": "active"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file was uploaded",
"example": "2025-01-15T10:30:00Z"
},
"expires_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file will expire (60 days from creation)",
"example": "2025-03-16T10:30:00Z"
},
"video": {
"type": [
"object",
"null"
],
"properties": {
"status": {
"type": "string",
"enum": [
"pending",
"processing",
"ready",
"failed"
],
"description": "Roll-up of the probe and every rendition. `ready` means a post using this file will publish without waiting. `pending`/`processing` means an encode is still in flight. `failed` means the probe or an encode gave up, and publishing falls back to the file you uploaded.",
"example": "ready"
},
"source_status": {
"type": "string",
"enum": [
"pending",
"probing",
"probed",
"probe_failed"
],
"description": "State of the source inspection. `probe_failed` means the file could not be read, so no renditions were planned.",
"example": "probed"
},
"error": {
"type": [
"string",
"null"
],
"description": "Why the probe failed, when source_status is `probe_failed`.",
"example": null
},
"probe": {
"type": [
"object",
"null"
],
"properties": {
"container_format": {
"type": "string",
"example": "mov,mp4,m4a,3gp,3g2,mj2"
},
"duration_ms": {
"type": "number",
"example": 70800
},
"width": {
"type": "number",
"description": "Stored width, before any container rotation is applied.",
"example": 1920
},
"height": {
"type": "number",
"example": 1080
},
"fps": {
"type": "number",
"example": 30
},
"video_codec": {
"type": "string",
"example": "h264"
},
"audio_codec": {
"type": [
"string",
"null"
],
"description": "Null for a silent video.",
"example": "aac"
},
"size_bytes": {
"type": "number",
"example": 18408940
},
"rotation": {
"type": "number",
"description": "Display rotation in degrees from container metadata: 0, 90, 180 or 270.",
"example": 0
},
"moov_at_front": {
"type": "boolean",
"description": "Instagram rejects files whose moov atom trails the media data.",
"example": true
},
"has_edit_lists": {
"type": "boolean",
"description": "Instagram rejects files with edit lists.",
"example": false
}
},
"required": [
"container_format",
"duration_ms",
"width",
"height",
"fps",
"video_codec",
"audio_codec",
"size_bytes",
"rotation",
"moov_at_front",
"has_edit_lists"
],
"description": "What ffprobe read off the file. Null until the probe completes."
},
"renditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "`master` is the network-agnostic normalized encode. Any other value is a network name, produced because the master would still breach that network's caps.",
"example": "master"
},
"status": {
"type": "string",
"enum": [
"queued",
"processing",
"ready",
"failed"
],
"example": "ready"
},
"url": {
"type": [
"string",
"null"
],
"description": "Public URL of the encoded file, once ready. This is what the publish path sends to the network in place of your original.",
"example": "https://media.outstand.so/renditions/abc123/568/master.mp4"
},
"error": {
"type": [
"string",
"null"
],
"example": null
}
},
"required": [
"kind",
"status",
"url",
"error"
]
},
"description": "An empty array is a success, not a gap: it means the file already satisfied every target and nothing needed re-encoding. Renditions are only produced for posts that opt into `processMedia`."
}
},
"required": [
"status",
"source_status",
"error",
"probe",
"renditions"
],
"description": "Video pipeline state, or null when this file is not a tracked video."
}
},
"required": [
"id",
"filename",
"url",
"content_type",
"size",
"status",
"created_at",
"expires_at",
"video"
],
"description": "Media file object"
}
},
"required": [
"success",
"data"
],
"description": "Response containing a single media file"
}
```
### 403
Unauthorized - media file belongs to different organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Media file not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Failed to generate upload URL"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/media/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/media/{id}', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get pending connection details (https://www.outstand.so/docs/get-pending-connection-details)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieve details of a pending OAuth connection, including available pages/accounts that can be connected. This endpoint is used during the OAuth finalization flow for platforms like LinkedIn and Facebook that support multiple pages/profiles.
**Authentication:** Requires your API key, *and* the session token must belong to the same organization as that key. A session token from another organization is rejected as if it had expired. Because the API key is required, call this from your backend - not from the browser your user was redirected to.
**Tokens are never returned.** The response describes each page only well enough to render a picker (`id`, `name`, `profilePictureUrl` and similar). Provider access tokens stay server-side and are attached to the account when you finalize.
**Flow:**
1. User completes OAuth authentication
2. Callback redirects to your page with `?session=xxx`
3. Your backend calls this endpoint to get available pages. Strip the `session` parameter from the browser URL once you have read it - it lands in history, logs and `Referer` headers otherwise
4. User selects which pages to connect
5. Call POST /finalize to complete the connection
## API Endpoint
`GET /v1/social-accounts/pending/{sessionToken}`
**Summary:** Get pending connection details
Retrieve details of a pending OAuth connection, including available pages/accounts that can be connected. This endpoint is used during the OAuth finalization flow for platforms like LinkedIn and Facebook that support multiple pages/profiles.
**Authentication:** Requires your API key, *and* the session token must belong to the same organization as that key. A session token from another organization is rejected as if it had expired. Because the API key is required, call this from your backend - not from the browser your user was redirected to.
**Tokens are never returned.** The response describes each page only well enough to render a picker (`id`, `name`, `profilePictureUrl` and similar). Provider access tokens stay server-side and are attached to the account when you finalize.
**Flow:**
1. User completes OAuth authentication
2. Callback redirects to your page with `?session=xxx`
3. Your backend calls this endpoint to get available pages. Strip the `session` parameter from the browser URL once you have read it - it lands in history, logs and `Referer` headers otherwise
4. User selects which pages to connect
5. Call POST /finalize to complete the connection
**Tags:** Pending Connections
## Parameters
- **sessionToken** (path: string) [required]
## Responses
### 200
Pending connection retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network type",
"example": "linkedin"
},
"expiresAt": {
"type": "number",
"description": "Unix timestamp when the session expires",
"example": 1734567890000
},
"availablePages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the page/account",
"example": "abc123"
},
"type": {
"type": "string",
"enum": [
"personal",
"organization",
"page",
"location"
],
"description": "Type of account",
"example": "organization"
},
"name": {
"type": "string",
"description": "Display name of the account",
"example": "Acme Inc"
},
"username": {
"type": "string",
"description": "Username or handle",
"example": "acme-inc"
},
"profilePictureUrl": {
"type": "string",
"description": "URL to the profile picture",
"example": "https://example.com/profile.jpg"
},
"category": {
"type": "string",
"description": "Facebook Page category",
"example": "Real Estate Agent"
},
"urn": {
"type": "string",
"description": "LinkedIn URN for the profile or organization",
"example": "urn:li:organization:12345"
},
"accountId": {
"type": "string",
"description": "Google Business Profile account the location belongs to",
"example": "accounts/1234567890"
},
"address": {
"type": "string",
"description": "Formatted address of a Google Business Profile location",
"example": "12 Example St, Sydney NSW 2000"
}
},
"required": [
"id",
"type",
"name",
"username"
],
"description": "Available page/account that can be connected. Never includes provider access tokens - the token needed to publish is held server-side and attached when you finalize."
}
}
},
"required": [
"network",
"expiresAt",
"availablePages"
]
}
},
"required": [
"success",
"data"
],
"description": "Pending connection data with available pages"
}
```
### 404
Session expired or invalid
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts/pending/{sessionToken} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/pending/{sessionToken}', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get Pinterest profile (https://www.outstand.so/docs/get-pinterest-profile)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Fetch the authenticated Pinterest user's account information (username, profile image, board/pin/follower counts, monthly views). This returns Pinterest-specific profile fields that don't fit the cross-network `GET /v1/social-accounts/{id}/metrics` response — use that endpoint for normalized follower/post counts.
## API Endpoint
`GET /v1/pinterest/accounts/{id}/profile`
**Summary:** Get Pinterest profile
Fetch the authenticated Pinterest user's account information (username, profile image, board/pin/follower counts, monthly views). This returns Pinterest-specific profile fields that don't fit the cross-network `GET /v1/social-accounts/{id}/metrics` response - use that endpoint for normalized follower/post counts.
**Tags:** Pinterest
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Profile retrieved
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"username": {
"type": "string",
"example": "myaccount"
},
"profile_image": {
"type": "string",
"example": "https://i.pinimg.com/..."
},
"website_url": {
"type": "string",
"example": "https://example.com"
},
"account_type": {
"type": "string",
"example": "BUSINESS"
},
"board_count": {
"type": "number",
"example": 12
},
"pin_count": {
"type": "number",
"example": 248
},
"follower_count": {
"type": "number",
"example": 1024
},
"following_count": {
"type": "number",
"example": 56
},
"monthly_views": {
"type": "number",
"example": 45000
}
},
"required": [
"username",
"profile_image"
],
"description": "Pinterest user account information"
}
},
"required": [
"success",
"data"
],
"description": "Pinterest profile response"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/pinterest/accounts/{id}/profile \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/pinterest/accounts/{id}/profile', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get post analytics (https://www.outstand.so/docs/get-post-analytics)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Fetch analytics and metrics for a published post across all social accounts it was published to. Returns both per-account metrics (likes, comments, shares, views, impressions, reach, engagement rate) and aggregated totals across all platforms. Only available for posts that have been published - draft or scheduled posts will return a 400 error. Platform-specific metrics (e.g., retweets for X, saves for Instagram) are included in the `platform_specific` field of each account's metrics.
Instagram Stories: `media_product_type` is `"STORY"` and the metric set differs from feed/reels - `likes`/`comments`/`saves` are not supported and returned as `null`, while `replies`, `profile_visits`, `follows`, `link_clicks`, `reposts`, `total_interactions` and a `navigation` breakdown (`tap_forward`, `tap_back`, `tap_exit`, `swipe_forward`) are populated instead. Story insights are only queryable for approximately 24 hours after publish; after that window this endpoint returns `metrics: null` with `metrics_error.code: "metrics_expired"` for that account.
Facebook: video and single-photo posts may initially have a media ID. When Meta provides a corresponding Post ID, Outstand resolves and caches it, so `platform_post_id` may differ from the ID returned at publish time. Unsupported or unreported engagement counts are `null` and named in `platform_specific.unavailable_fields`; measured zero remains `0`. Available counts are returned even when other fields or supplementary insights are unavailable. `engagement_total` and `engagement_rate` are `null` when required engagement counts are unavailable. Aggregated totals sum only available measurements and can be partial; engagement-rate averages include measured zeros and exclude unavailable rates.
## API Endpoint
`GET /v1/posts/{id}/analytics`
**Summary:** Get post analytics
Fetch analytics and metrics for a published post across all social accounts it was published to. Returns both per-account metrics (likes, comments, shares, views, impressions, reach, engagement rate) and aggregated totals across all platforms. Only available for posts that have been published - draft or scheduled posts will return a 400 error. Platform-specific metrics (e.g., retweets for X, saves for Instagram) are included in the `platform_specific` field of each account's metrics.
Instagram Stories: `media_product_type` is `"STORY"` and the metric set differs from feed/reels - `likes`/`comments`/`saves` are not supported and returned as `null`, while `replies`, `profile_visits`, `follows`, `link_clicks`, `reposts`, `total_interactions` and a `navigation` breakdown (`tap_forward`, `tap_back`, `tap_exit`, `swipe_forward`) are populated instead. Story insights are only queryable for approximately 24 hours after publish; after that window this endpoint returns `metrics: null` with `metrics_error.code: "metrics_expired"` for that account.
Facebook: video and single-photo posts may initially have a media ID. When Meta provides a corresponding Post ID, Outstand resolves and caches it, so `platform_post_id` may differ from the ID returned at publish time. Unsupported or unreported engagement counts are `null` and named in `platform_specific.unavailable_fields`; measured zero remains `0`. Available counts are returned even when other fields or supplementary insights are unavailable. `engagement_total` and `engagement_rate` are `null` when required engagement counts are unavailable. Aggregated totals sum only available measurements and can be partial; engagement-rate averages include measured zeros and exclude unavailable rates.
**Tags:** Posts, Analytics
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Analytics retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"post": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "9dyJS"
},
"publishedAt": {
"type": "string",
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"publishedAt",
"createdAt"
]
},
"metrics_by_account": {
"type": "array",
"items": {
"type": "object",
"properties": {
"social_account": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"nickname": {
"type": "string"
},
"network": {
"type": "string"
},
"username": {
"type": "string"
}
},
"required": [
"id",
"nickname",
"network",
"username"
]
},
"platform_post_id": {
"type": "string"
},
"platform_post_url": {
"type": [
"string",
"null"
]
},
"published_at": {
"type": "string",
"format": "date-time"
},
"metrics": {
"type": [
"object",
"null"
],
"properties": {
"likes": {
"type": [
"number",
"null"
]
},
"comments": {
"type": [
"number",
"null"
]
},
"shares": {
"type": [
"number",
"null"
]
},
"views": {
"type": "number"
},
"impressions": {
"type": [
"number",
"null"
]
},
"reach": {
"type": [
"number",
"null"
]
},
"engagement_rate": {
"type": [
"number",
"null"
]
},
"engagement_total": {
"type": [
"number",
"null"
]
},
"media_product_type": {
"type": "string",
"enum": [
"FEED",
"REELS",
"STORY"
]
},
"replies": {
"type": "number"
},
"profile_visits": {
"type": "number"
},
"follows": {
"type": "number"
},
"link_clicks": {
"type": "number"
},
"reposts": {
"type": "number"
},
"total_interactions": {
"type": "number"
},
"navigation": {
"type": "object",
"properties": {
"tap_forward": {
"type": "number"
},
"tap_back": {
"type": "number"
},
"tap_exit": {
"type": "number"
},
"swipe_forward": {
"type": "number"
}
},
"description": "Instagram Story navigation breakdown (tap_forward, tap_back, tap_exit, swipe_forward)"
},
"platform_specific": {
"type": "object",
"additionalProperties": {}
}
},
"description": "Analytics metrics for a post on a specific platform"
},
"metrics_error": {
"type": "object",
"properties": {
"code": {
"type": "string",
"enum": [
"token_expired",
"scope_missing",
"post_not_found",
"metrics_expired",
"unknown"
]
},
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
],
"description": "Reason metrics could not be fetched for this account (present only when metrics is null). metrics_expired means the content (e.g. an Instagram Story) is no longer queryable - Stories only expose insights for ~24h after publish."
}
},
"required": [
"social_account",
"platform_post_id",
"platform_post_url",
"published_at",
"metrics"
],
"description": "Analytics metrics for a post on a specific social account"
}
},
"aggregated_metrics": {
"type": "object",
"properties": {
"total_likes": {
"type": "number"
},
"total_comments": {
"type": "number"
},
"total_shares": {
"type": "number"
},
"total_views": {
"type": "number"
},
"total_impressions": {
"type": "number"
},
"total_reach": {
"type": "number"
},
"average_engagement_rate": {
"type": "number"
}
},
"required": [
"total_likes",
"total_comments",
"total_shares",
"total_views",
"total_impressions",
"total_reach",
"average_engagement_rate"
],
"description": "Sums of available measurements across platforms; totals can be partial. The average includes measured zero rates and excludes unavailable rates."
}
},
"required": [
"success",
"post",
"metrics_by_account",
"aggregated_metrics"
],
"description": "Post analytics response with per-account and aggregated metrics"
}
```
### 400
Post is not published (draft or scheduled)
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 403
Unauthorized - post does not belong to your organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/posts/{id}/analytics \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/analytics', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get post details (https://www.outstand.so/docs/get-post-details)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Get detailed information about a specific post by its ID, including all containers (content segments), attached media files, scheduling status, and publishing state. The post ID is the encoded string identifier returned when creating or listing posts. The response includes the full container hierarchy with media URLs for each segment.
## API Endpoint
`GET /v1/posts/{id}`
**Summary:** Get post details
Get detailed information about a specific post by its ID, including all containers (content segments), attached media files, scheduling status, and publishing state. The post ID is the encoded string identifier returned when creating or listing posts. The response includes the full container hierarchy with media URLs for each segment.
Media URLs are always the file you uploaded. When a post opts into `processMedia`, videos are normalized in the background and the publish path substitutes the encoded file per network - it never rewrites the source URL stored on the post. Use `containers[].media[].processing` to see that pipeline: the roll-up `status`, the ffprobe reading, each rendition and its public URL, and the per-network `issues` the source would otherwise hit. It is `null` for images and for videos on posts that did not opt in.
**Tags:** Posts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Post retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"post": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"example": "abc123"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"scheduledAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isDraft": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"socialAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the social account",
"example": "9dyJS"
},
"nickname": {
"type": "string",
"description": "User-defined nickname for this social account"
},
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'facebook', 'x', 'instagram', 'linkedin', 'threads', 'youtube', 'tiktok')"
},
"username": {
"type": "string",
"description": "Username or handle on the social network"
},
"status": {
"type": "string",
"enum": [
"pending",
"published",
"failed",
"deleted"
],
"description": "Publishing status for this account. 'pending': awaiting publish (post created or scheduled), 'published': successfully published to this account, 'failed': publishing failed (check error field), 'deleted': post was deleted from the social network platform. Note: A post can partially succeed - some accounts may publish while others fail.",
"example": "published"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message if publishing failed. Contains the platform-specific error message. Common errors include: expired access tokens, rate limits, invalid media format, content policy violations. Null if status is not 'failed'.",
"example": null
},
"platformPostId": {
"type": [
"string",
"null"
],
"description": "The platform-specific post ID after successful publish. Use this for analytics, fetching comments, or deep-linking to the post on the platform. Null if not yet published or if publishing failed.",
"example": "123456789"
},
"platformPostUrl": {
"type": [
"string",
"null"
],
"description": "The public URL of the post on the social network. Currently populated for Instagram only; null for other networks or for posts created before this feature.",
"example": "https://www.instagram.com/p/DAbCdEfGhIj/"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when the post was successfully published to this account. Null if pending or failed. Each account has its own publishedAt since they are processed sequentially.",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"id",
"nickname",
"network",
"username",
"status",
"error",
"platformPostId",
"platformPostUrl",
"publishedAt"
]
}
},
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "8xKmL"
},
"content": {
"type": "string"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"filename": {
"type": "string"
},
"processing": {
"type": [
"object",
"null"
],
"properties": {
"status": {
"type": "string",
"enum": [
"pending",
"processing",
"ready",
"failed"
],
"description": "Roll-up of the probe and every rendition. `ready` means publishing will use the best available file and will not wait. `pending`/`processing` means a publish attempt defers until the encode settles or the deadline passes. `failed` means the probe or an encode gave up, and publishing falls back to the file you uploaded.",
"example": "ready"
},
"sourceStatus": {
"type": "string",
"enum": [
"pending",
"probing",
"probed",
"probe_failed"
],
"description": "State of the source inspection. `probe_failed` means we could not read the file at all, so no renditions were planned.",
"example": "probed"
},
"error": {
"type": [
"string",
"null"
],
"description": "Why the probe failed, when sourceStatus is `probe_failed`.",
"example": null
},
"probe": {
"type": [
"object",
"null"
],
"properties": {
"containerFormat": {
"type": "string",
"example": "mov,mp4,m4a,3gp,3g2,mj2"
},
"durationMs": {
"type": "number",
"example": 70800
},
"width": {
"type": "number",
"description": "Stored width, before any container rotation is applied.",
"example": 1920
},
"height": {
"type": "number",
"example": 1080
},
"fps": {
"type": "number",
"example": 30
},
"videoCodec": {
"type": "string",
"example": "h264"
},
"audioCodec": {
"type": [
"string",
"null"
],
"description": "Null for a silent video.",
"example": "aac"
},
"sizeBytes": {
"type": "number",
"example": 18408940
},
"rotation": {
"type": "number",
"description": "Display rotation in degrees from container metadata: 0, 90, 180 or 270.",
"example": 0
},
"moovAtFront": {
"type": "boolean",
"description": "Instagram rejects files whose moov atom trails the media data.",
"example": true
},
"hasEditLists": {
"type": "boolean",
"description": "Instagram rejects files with edit lists.",
"example": false
}
},
"required": [
"containerFormat",
"durationMs",
"width",
"height",
"fps",
"videoCodec",
"audioCodec",
"sizeBytes",
"rotation",
"moovAtFront",
"hasEditLists"
],
"additionalProperties": true,
"description": "What ffprobe read off the source. Null until the probe completes."
},
"renditions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "`master` is the network-agnostic normalized encode. Any other value is a network name, produced because the master would still breach that network's caps.",
"example": "master"
},
"status": {
"type": "string",
"enum": [
"queued",
"processing",
"ready",
"failed"
],
"example": "ready"
},
"url": {
"type": [
"string",
"null"
],
"description": "Public URL of the encoded file, once ready. This is what the publish path sends to the network in place of your original.",
"example": "https://media.outstand.so/renditions/abc123/568/master.mp4"
},
"error": {
"type": [
"string",
"null"
],
"example": null
}
},
"required": [
"kind",
"status",
"url",
"error"
]
},
"description": "An empty array is a success, not a gap: it means the source already satisfied every target network and nothing needed re-encoding."
},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": [
"error",
"warning"
],
"description": "`error` blocks post creation. `warning` does not - either a rendition fixes it, or you chose to publish as-is.",
"example": "warning"
},
"code": {
"type": "string",
"example": "file_too_large"
},
"message": {
"type": "string",
"example": "Video is 512 MB, above the 100 MB limit for bluesky."
},
"network": {
"type": "string",
"example": "bluesky"
},
"fixableByTranscode": {
"type": "boolean",
"description": "False for duration and aspect-ratio breaches - fixing those would mean trimming or cropping your content, which we do not do on your behalf.",
"example": true
}
},
"required": [
"level",
"code",
"message",
"network",
"fixableByTranscode"
]
},
"description": "What the post's target networks object to, derived from the probe. Recomputed on read, so it reflects the accounts the post targets now."
}
},
"required": [
"status",
"sourceStatus",
"error",
"probe",
"renditions",
"issues"
],
"description": "Video processing state for this file, or null when it is not a tracked video."
}
},
"required": [
"url",
"filename",
"processing"
]
}
},
"publishResults": {
"type": "array",
"items": {
"type": "object",
"properties": {
"accountId": {
"type": "string",
"description": "The social account this outcome belongs to.",
"example": "9dyJS"
},
"status": {
"type": "string",
"enum": [
"published",
"failed"
],
"example": "published"
},
"platformCommentId": {
"type": [
"string",
"null"
],
"description": "Platform-specific ID of the posted comment/reply.",
"example": "urn:li:comment:(urn:li:share:123,456)"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message if this container failed to publish for the account.",
"example": null
},
"publishedAt": {
"type": "string",
"format": "date-time",
"description": "When this outcome was recorded."
}
},
"required": [
"accountId",
"status",
"platformCommentId",
"error",
"publishedAt"
]
},
"description": "Per-account publish outcome for non-root containers (first comments/replies). Empty for the root container, whose status is reflected by the post's socialAccounts."
}
},
"required": [
"id",
"content",
"media",
"publishResults"
]
}
}
},
"required": [
"id",
"orgId",
"publishedAt",
"scheduledAt",
"isDraft",
"createdAt",
"socialAccounts",
"containers"
]
}
},
"required": [
"success",
"post"
]
}
```
### 403
Unauthorized - post does not belong to your organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/posts/{id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get post replies/comments (https://www.outstand.so/docs/get-post-repliescomments)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Fetch all comments and replies for a specific published post from a particular social media account. Specify the target account using `network` (required) and optionally `username`. If the post was published to only one account on that network, `username` can be omitted. When the post was published to multiple accounts on the same network, omitting `username` returns a 400 with the available usernames to disambiguate. Returns the full thread of replies including author information, content, and timestamps. Comments are not supported on every network - TikTok, Pinterest, and Google Business Profile do not expose a comments API, and YouTube comments are not yet supported; these networks will return an error.
## API Endpoint
`GET /v1/posts/{id}/replies`
**Summary:** Get post replies/comments
Fetch all comments and replies for a specific published post from a particular social media account. Specify the target account using `network` (required) and optionally `username`. If the post was published to only one account on that network, `username` can be omitted. When the post was published to multiple accounts on the same network, omitting `username` returns a 400 with the available usernames to disambiguate. By default only top-level comments are returned; set `include_replies=true` to nest each comment's replies under it (Facebook, Instagram, LinkedIn - a single reply tier; Threads nests to any depth). Note that Threads returns the whole conversation flat when `include_replies` is omitted, so nested replies still appear there as top-level entries. Comments are not supported on every network - TikTok, Pinterest, and Google Business Profile do not expose a comments API, and YouTube comments are not yet supported; these networks will return an error.
**Response keys:** read `data`, which returns the same comment shape for every network (id, author, text, created_at, like_count, and nested `replies` when requested). The `replies` key is deprecated: it preserves the per-network shape this endpoint returned before the cross-network format existed, so it differs by network and never carries nested replies. It is kept only so existing integrations do not break.
**Tags:** Posts, Comments
## Parameters
- **network** (query: string) [required]: Social network name (e.g., 'x', 'threads')
- **username** (query: string): Username or nickname of the social account that published the post. Optional when the post was published to only one account on the given network.
- **include_replies** (query: boolean): When 'true', nest each comment's replies under it (post → comment → replies). Defaults to top-level comments only. Fetching replies on LinkedIn issues an extra API call per comment.
- **resolve_author_names** (query: boolean): LinkedIn only: when 'true', resolve each commenter's URN into `author_name` (plus `author_username`, `author_avatar_url` and `author_profile_url` where LinkedIn provides them). Every other network already returns a readable author and ignores this. Some LinkedIn commenters cannot be resolved (for example members who limit their off-LinkedIn visibility) - those come back with `author_name: null` and `author` left as the URN. LinkedIn does not allow member profile data to be stored: cache a resolved name for at most 24 hours and re-fetch after that.
- **id** (path: string) [required]
## Responses
### 200
Replies retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"replies": {
"anyOf": [
{
"type": "array",
"items": {
"$ref": "#/components/schemas/LegacyNormalizedReply"
}
},
{
"$ref": "#/components/schemas/LegacyFacebookReplies"
},
{
"$ref": "#/components/schemas/LegacyInstagramReplies"
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/LegacyBlueskyReply"
}
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/LegacyVimeoComment"
}
}
],
"deprecated": true,
"description": "Deprecated. The per-network shape this endpoint returned before the cross-network reply format was introduced, frozen so existing integrations keep working. The shape depends on `network`: Facebook and Instagram return an object `{ comments: [...] }`; Bluesky and Vimeo return arrays with network-specific field names (note Bluesky's `replies` here is a reply count, not an array); every other network returns the same array as `data`. Nested replies never appear here, even with include_replies=true. Use `data`, which is identical across every network."
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NormalizedReply"
},
"description": "Top-level comments in the cross-network shape, each optionally carrying nested replies when include_replies=true. Identical for every network. Nesting is one tier deep on Facebook, Instagram, and LinkedIn, and unbounded on Threads. Prefer this over `replies`."
}
},
"required": [
"success",
"replies",
"data"
]
}
```
### 400
Invalid query parameters
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 403
Unauthorized - post belongs to different organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found or account not published
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/posts/{id}/replies \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/replies', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Get social network authentication URL (https://www.outstand.so/docs/get-social-network-authentication-url)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Generate an OAuth authentication URL for connecting a social media account. This endpoint returns a URL that you should redirect users to in order to authenticate and authorize your application to access their social media account.
**Prerequisites:**
* The social network must be configured for your organization using the POST /v1/social-networks endpoint
* You must have valid OAuth credentials (client\_key and client\_secret) stored for the specified network
**Workflow:**
1. Ensure the social network is configured for your organization
2. Call this endpoint to get the authentication URL (optionally include tenant\_id to associate accounts with your end-users)
3. Redirect the user to the returned auth\_url
4. The user will authenticate on the social network's platform
5. The user will be redirected back to your specified redirect\_uri (or default)
6. Handle the OAuth callback to complete the account connection
**Tenant ID:** Optionally provide a tenant\_id to associate the connected social account with a specific tenant/customer in your system. This enables filtering social accounts by tenant using GET /v1/social-accounts?tenantId=xxx. The tenant\_id must contain only alphanumeric characters, underscores, and hyphens.
**Redirect URI:** If you provide a redirect\_uri in the request body, it is the final application destination after Outstand completes the connection, not the OAuth callback registered with the provider. For YouTube BYOK, configure oauth\_callback\_url on the credential separately. If redirect\_uri is omitted, the user returns to the Outstand dashboard.
**Connecting multiple accounts on the same network:** Most networks skip the authorization screen and silently re-use the account already signed in on the user's browser session. That means a user connecting their second account can end up re-authorizing the first one, with logging out of the network as the only workaround. Set `force_account_selection: true` to always show the authorization / account-selection screen. It is honoured by tiktok, facebook, instagram, youtube and google\_business, and ignored by networks that expose no equivalent, so it is safe to send on every request.
**Instagram Direct Messages:** Messaging access is not requested by default. To use the Conversations API, set `scopes` to `instagram_business_basic,instagram_business_content_publish,instagram_business_manage_comments,instagram_business_manage_insights,instagram_business_manage_messages`. Custom `scopes` replace the defaults, so keep the first four unless you deliberately want a messaging-only connection. Accounts connected without `instagram_business_manage_messages` do not receive DMs and must be reconnected with it.
**Important**: For white-label users, this is a very important endpoint as it guarantees your customers never see our branding in the authentication flow.
## API Endpoint
`POST /v1/social-networks/{network}/auth-url`
**Summary:** Get social network authentication URL
Generate an OAuth authentication URL for connecting a social media account. This endpoint returns a URL that you should redirect users to in order to authenticate and authorize your application to access their social media account.
**Prerequisites:**
- The social network must be configured for your organization using the POST /v1/social-networks endpoint
- You must have valid OAuth credentials (client_key and client_secret) stored for the specified network
**Workflow:**
1. Ensure the social network is configured for your organization
2. Call this endpoint to get the authentication URL (optionally include tenant_id to associate accounts with your end-users)
3. Redirect the user to the returned auth_url
4. The user will authenticate on the social network's platform
5. The user will be redirected back to your specified redirect_uri (or default)
6. Handle the OAuth callback to complete the account connection
**Tenant ID:** Optionally provide a tenant_id to associate the connected social account with a specific tenant/customer in your system. This enables filtering social accounts by tenant using GET /v1/social-accounts?tenantId=xxx. The tenant_id must contain only alphanumeric characters, underscores, and hyphens.
**Redirect URI:** If you provide a redirect_uri in the request body, it is the final application destination after Outstand completes the connection, not the OAuth callback registered with the provider. For YouTube BYOK, configure oauth_callback_url on the credential separately. If redirect_uri is omitted, the user returns to the Outstand dashboard.
**Connecting multiple accounts on the same network:** Most networks skip the authorization screen and silently re-use the account already signed in on the user's browser session. That means a user connecting their second account can end up re-authorizing the first one, with logging out of the network as the only workaround. Set `force_account_selection: true` to always show the authorization / account-selection screen. It is honoured by tiktok, facebook, instagram, youtube and google_business, and ignored by networks that expose no equivalent, so it is safe to send on every request.
**Instagram Direct Messages:** Messaging access is not requested by default. To use the Conversations API, set `scopes` to `instagram_business_basic,instagram_business_content_publish,instagram_business_manage_comments,instagram_business_manage_insights,instagram_business_manage_messages`. Custom `scopes` replace the defaults, so keep the first four unless you deliberately want a messaging-only connection. Accounts connected without `instagram_business_manage_messages` do not receive DMs and must be reconnected with it.
**Important**: For white-label users, this is a very important endpoint as it guarantees your customers never see our branding in the authentication flow.
**Tags:** Customer Social Networks
## Parameters
- **network** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"redirect_uri": {
"type": "string",
"format": "uri",
"description": "Optional redirect URI for successful OAuth callback. If not provided, a default redirect URI will be used - which redirects in our management dashboard. The redirect URI usually is used to redirect somewhere in your own application to continue the user journey.",
"example": "https://my.site.com/onboard/callback"
},
"tenant_id": {
"type": "string",
"description": "Optional customer-provided tenant identifier. This will be associated with any social accounts created during this OAuth flow, enabling you to filter accounts by tenant later using the GET /v1/social-accounts endpoint.",
"example": "customer_123"
},
"scopes": {
"type": "string",
"description": "Optional comma-separated list of OAuth scopes to request for this network. When provided, ONLY these scopes are requested, overriding the defaults. When omitted, the default scopes for the network are used. Useful when bringing your own OAuth keys and your app only has a subset of products or permissions enabled. Instagram Direct Messages are opt-in: the default Instagram scopes (instagram_business_basic, instagram_business_content_publish, instagram_business_manage_comments, instagram_business_manage_insights) do not include instagram_business_manage_messages, so to use the Conversations API pass all four defaults plus instagram_business_manage_messages here. Accounts connected without it receive no DMs and must be reconnected with it.",
"example": "pins:read,pins:write,boards:read"
},
"force_account_selection": {
"type": "boolean",
"description": "Optional. When true, the OAuth flow always shows the network's authorization / account-selection screen instead of silently re-using the account already signed in on the user's browser session. Set this when your users connect more than one account on the same network - otherwise the second connection can silently re-authorize the first account and the user has to log out of the network to switch. Honoured by tiktok (disable_auto_auth), facebook (auth_type=reauthenticate), instagram (force_reauth), youtube and google_business (prompt=select_account). Networks with no equivalent (linkedin, x, pinterest, threads, vimeo, bluesky) ignore it, so it is safe to send unconditionally.",
"example": true
}
},
"description": "Request schema for generating authentication URL"
}
```
## Responses
### 200
Authentication URL generated successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"auth_url": {
"type": "string",
"format": "uri",
"description": "OAuth authorization URL that the user should be redirected to in order to authenticate and authorize the application. The URL is unique per organization, social network, and optionally tenant. Do not cache this URL - always generate a fresh one per authentication request, especially when using tenant_id.",
"example": "https://www.outstand.so/app/api/socials/instagram/:orgId?state=..."
}
},
"required": [
"auth_url"
],
"description": "Authentication URL data"
}
},
"required": [
"success",
"data"
],
"description": "Successful authentication URL response"
}
```
### 400
Invalid network type or request payload
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Social network not configured for your organization
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"details": {
"example": {
"network": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/social-networks/{network}/auth-url \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"redirect_uri": "https://my.site.com/onboard/callback",
"tenant_id": "customer_123",
"scopes": "pins:read,pins:write,boards:read",
"force_account_selection": true
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-networks/{network}/auth-url', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"redirect_uri": "https://my.site.com/onboard/callback",
"tenant_id": "customer_123",
"scopes": "pins:read,pins:write,boards:read",
"force_account_selection": true
})
});
const data = await response.json();
```
# Get upload URL (https://www.outstand.so/docs/get-upload-url)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Request a presigned URL for uploading a media file directly to storage. The returned URL is valid for 1 hour.
**Upload Process:**
1. Call this endpoint with the filename
2. Use the returned `upload_url` to upload your file via HTTP PUT
3. After successful upload, call `POST /v1/media/{id}/confirm` to finalize
**Example upload with curl:**
```bash
curl -X PUT -T "your-file.jpg" -H "Content-Type: image/jpeg" ""
```
## API Endpoint
`POST /v1/media/upload`
**Summary:** Get upload URL
Request a presigned URL for uploading a media file directly to storage. The returned URL is valid for 1 hour.
**Upload Process:**
1. Call this endpoint with the filename
2. Use the returned `upload_url` to upload your file via HTTP PUT
3. After successful upload, call `POST /v1/media/{id}/confirm` to finalize
**Example upload with curl:**
```bash
curl -X PUT -T "your-file.jpg" -H "Content-Type: image/jpeg" ""
```
**Tags:** Media
## Request Body
```json
{
"type": "object",
"properties": {
"filename": {
"type": "string",
"minLength": 1,
"maxLength": 255,
"description": "The filename for the media file. Should include the file extension.",
"example": "product-image.jpg"
},
"content_type": {
"type": "string",
"minLength": 1,
"description": "The MIME type of the file. If not provided, it will be inferred from the filename extension.",
"example": "image/jpeg"
}
},
"required": [
"filename"
],
"description": "Request schema for getting a presigned upload URL"
}
```
## Responses
### 200
Upload URL generated successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the pending media file",
"example": "9dyJS"
},
"upload_url": {
"type": "string",
"format": "uri",
"description": "Presigned URL for uploading the file directly to storage. Valid for 1 hour.",
"example": "https://media-bucket.r2.cloudflarestorage.com/..."
},
"expires_in": {
"type": "number",
"description": "Time in seconds until the upload URL expires",
"example": 3600
}
},
"required": [
"id",
"upload_url",
"expires_in"
]
}
},
"required": [
"success",
"data"
],
"description": "Response containing presigned upload URL"
}
```
### 400
Invalid request payload
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid request"
},
"details": {
"example": {
"filename": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Failed to generate upload URL"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "product-image.jpg",
"content_type": "image/jpeg"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/media/upload', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"filename": "product-image.jpg",
"content_type": "image/jpeg"
})
});
const data = await response.json();
```
# Getting Started (https://www.outstand.so/docs/getting-started)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
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](/conversations) 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 | `linkedin` | OAuth | Managed Keys |
| Instagram | `instagram` | OAuth | Managed Keys |
| Facebook | `facebook` | OAuth | Managed Keys |
| Threads | `threads` | OAuth | Managed Keys |
| TikTok | `tiktok` | OAuth | Managed Keys |
| YouTube | `youtube` | OAuth | Managed Keys |
| Pinterest | `pinterest` | OAuth | Managed Keys |
| Google Business Profile | `google_business` | OAuth | BYOK required |
| Vimeo | `vimeo` | OAuth | BYOK required |
| Reddit | `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](#1-connect-an-account) 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](/configurations) for per-network setup guides.
## Creating an account
To sign up for an account, you can go to the [Outstand website](https://www.outstand.so/app/signup) 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](/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/callback
```
Replace `{network}` with a value from [Supported Platforms](#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=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:
```bash
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:
```bash
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.
```bash
# 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" }'
```
```json
{
"success": true,
"data": {
"id": "Vn4kP",
"upload_url": "https://…r2…/kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe/7c9a1b42-6d3e-4f18-9a52-2b7c4e8d1f03/launch.jpg?X-Amz-Signature=…",
"expires_in": 3600
}
}
```
```bash
# 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:
```json
{
"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](#targeting-accounts) for why. Attach media by passing
its `url` and a `filename` on the container:
```bash
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"]
}'
```
```json
{
"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](#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:
```bash
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:
```json
{
"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](#targeting-accounts):
```bash
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:
```bash
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:
```bash
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.
```bash
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:
```json
{
"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`:
```bash
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:
```json
{
"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:
```bash
curl -X GET https://api.outstand.so/v1/posts/9dyJS \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json
{
"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:
```bash
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.
```typescript
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:
```python
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](https://www.outstand.so) 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.
# Hide or unhide a comment (https://www.outstand.so/docs/hide-or-unhide-a-comment)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Hide or unhide a comment on an Instagram post managed by Outstand. Provide Outstand's post ID in the path and the comment's Instagram-specific ID as commentId, and set `hidden` in the body. Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
Hiding is the only mutable operation the Instagram Graph API exposes on a published comment - comment text cannot be edited via the API.
## API Endpoint
`PATCH /v1/instagram/posts/{postId}/comments/{commentId}`
**Summary:** Hide or unhide a comment
Hide or unhide a comment on an Instagram post managed by Outstand. Provide Outstand's post ID in the path and the comment's Instagram-specific ID as commentId, and set `hidden` in the body. Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
Hiding is the only mutable operation the Instagram Graph API exposes on a published comment - comment text cannot be edited via the API.
**Tags:** Instagram, Comments
## Parameters
- **postId** (path: string) [required]
- **commentId** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"hidden": {
"type": "boolean",
"description": "Set to true to hide the comment, or false to unhide it.",
"example": true
},
"platform_post_id": {
"type": "string",
"description": "The platform-specific post ID of the account whose credentials should be used. Optional when the post was published to only one account.",
"example": "123456789"
},
"account_username": {
"type": "string",
"description": "The username or nickname of the connected account whose credentials should be used. Optional when the post was published to only one account.",
"example": "mycompany"
}
},
"required": [
"hidden"
],
"description": "Update comment visibility request"
}
```
## Responses
### 200
Comment visibility updated
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if the comment's visibility was successfully updated on Instagram.",
"example": true
}
},
"required": [
"success"
],
"description": "Update comment visibility response"
}
```
### 400
Invalid request
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post or matching social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X PATCH https://api.outstand.so/v1/instagram/posts/{postId}/comments/{commentId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"hidden": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/instagram/posts/{postId}/comments/{commentId}', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"hidden": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
})
});
const data = await response.json();
```
# Hide or unhide a reply (https://www.outstand.so/docs/hide-or-unhide-a-reply)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Hide or unhide a reply on a Threads post managed by Outstand. Provide Outstand's post ID in the path and the reply's Threads-specific ID as replyId, and set `hidden` in the body. Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
Note: Threads does not support editing a reply's text via the API.
## API Endpoint
`PATCH /v1/threads/posts/{postId}/replies/{replyId}`
**Summary:** Hide or unhide a reply
Hide or unhide a reply on a Threads post managed by Outstand. Provide Outstand's post ID in the path and the reply's Threads-specific ID as replyId, and set `hidden` in the body. Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account).
Note: Threads does not support editing a reply's text via the API.
**Tags:** Threads, Replies
## Parameters
- **postId** (path: string) [required]
- **replyId** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"hidden": {
"type": "boolean",
"description": "Set to true to hide the reply, or false to unhide it.",
"example": true
},
"platform_post_id": {
"type": "string",
"description": "The platform-specific post ID of the account whose credentials should be used. Optional when the post was published to only one account.",
"example": "123456789"
},
"account_username": {
"type": "string",
"description": "The username or nickname of the connected account whose credentials should be used. Optional when the post was published to only one account.",
"example": "mycompany"
}
},
"required": [
"hidden"
],
"description": "Update reply visibility request"
}
```
## Responses
### 200
Reply visibility updated
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if the reply's visibility was successfully updated on Threads.",
"example": true
}
},
"required": [
"success"
],
"description": "Update reply visibility response"
}
```
### 400
Invalid request
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post or matching social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X PATCH https://api.outstand.so/v1/threads/posts/{postId}/replies/{replyId} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"hidden": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/threads/posts/{postId}/replies/{replyId}', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"hidden": true,
"platform_post_id": "123456789",
"account_username": "mycompany"
})
});
const data = await response.json();
```
# Idempotency (https://www.outstand.so/docs/idempotency)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
When a request times out or the connection drops, you cannot tell whether the server processed it. Retrying is risky - the post may already have been created and published. Not retrying is also risky, because the post may never have been created at all.
Idempotency keys remove the guesswork. Send a key with your request and you can retry it as many times as you like: the first attempt does the work, and every later attempt with the same key returns that first attempt's result instead of creating another post.
## Supported endpoints
| Endpoint | Idempotency |
| ---------------- | ----------- |
| `POST /v1/posts` | Supported |
Other endpoints ignore the header today. `GET` and `DELETE` requests do not need it - they are already safe to repeat.
## Sending a key
Add an `Idempotency-Key` header. Any printable ASCII string up to 255 characters works, but a UUID v4 is the right choice because it is collision-free without any coordination on your side.
```bash
curl -X POST https://api.outstand.so/v1/posts \
-H "Authorization: Bearer $OUTSTAND_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: b1f6c3f4-8f0e-4a5b-9c2d-7e1a0b3c4d5e" \
-d '{
"content": "Shipping something new today.",
"accounts": ["YOUR_X_ACCOUNT_ID", "YOUR_LINKEDIN_ACCOUNT_ID"]
}'
```
Generate the key **once per logical operation, not once per HTTP attempt**. All retries of the same create must carry the same key, otherwise each one is a new request and you are back to duplicating posts.
```ts
// Correct: the key is created outside the retry loop.
const idempotencyKey = crypto.randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
const response = await fetch('https://api.outstand.so/v1/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.status === 409) {
// An earlier attempt is still being processed. Wait and retry the same key.
await new Promise((resolve) => setTimeout(resolve, Number(response.headers.get('Retry-After') ?? 1) * 1000));
continue;
}
return response;
}
```
Requests without the header behave exactly as before, so adding keys is a change you can roll out gradually.
## Replays
When a key has already produced a result, the API returns that stored response verbatim - same status code, same body - along with a header marking it as a replay:
```http
HTTP/1.1 200 OK
Idempotency-Replayed: true
Idempotency-Key: b1f6c3f4-8f0e-4a5b-9c2d-7e1a0b3c4d5e
```
If you are migrating from Stripe, note the spelling. Stripe sends `Idempotent-Replayed`; Outstand sends `Idempotency-Replayed`, matching the request header name.
A replayed body is reproduced from stored JSON, so the order of its keys is not guaranteed to match the original response. Read fields by name rather than relying on ordering.
## Retention
Keys are remembered for **24 hours** from first use, and are scoped to your organization and to the endpoint. Two things follow from this:
* Every API key belonging to the same organization shares one key namespace. A retry sent with a newly rotated API key still deduplicates correctly.
* After 24 hours the key is forgotten. Reusing it creates a **new** post rather than replaying the old one, so keys are not a long-term record of what you sent.
## Error responses
Every response below carries a machine-readable `code` alongside the human-readable `error`.
### 409 - a request with this key is still running
```json
{
"success": false,
"error": "A request with this Idempotency-Key is already in progress. Retry in a moment.",
"code": "idempotency_request_in_progress"
}
```
Your previous attempt has not finished yet. Wait for the interval in the `Retry-After` header and send the same key again; you will get the original result once it completes.
### 422 - the key was reused with a different body
```json
{
"success": false,
"error": "This Idempotency-Key was already used with a different request body. Use a new key for a different request.",
"code": "idempotency_key_reuse"
}
```
A key is bound to the exact request that claimed it, so it can never return a result for content you did not send. Reformatting your JSON or reordering its keys is fine - only a genuine change to the values counts as a different request. Use a fresh key for a different post.
### 400 - the key itself is unusable
```json
{
"success": false,
"error": "Idempotency-Key must be at most 255 characters",
"code": "invalid_idempotency_key"
}
```
Returned for an empty key, a key over 255 characters, or one containing control characters.
## Failures that already created a post
One case deserves attention because retrying is the wrong response to it.
A post is stored first and queued for publishing immediately afterwards. If the queueing step fails, the post exists but is not scheduled, and you receive:
```json
{
"success": false,
"post": { "id": "Xk3p9m", "scheduledAt": "2026-08-01T12:00:00.000Z", "...": "..." },
"scheduled": false,
"error": "Failed to schedule publishing task"
}
```
Because a post was created, this response is **stored and replayed** like any other result. Retrying with the same key returns this same error, not a new post - which is the point: retrying cannot leave you with two posts.
To resolve it, act on the post in the response rather than retrying the create:
* `PATCH /v1/posts/{id}` with a `scheduledAt` value to queue it again, or
* `DELETE /v1/posts/{id}` to discard it and start over with a new key.
If a request is interrupted before the post is stored, nothing is persisted and the key is released, so a straightforward retry with the same key succeeds normally.
If a request is interrupted *after* the post is stored — for example the connection dropped before the response reached you — a retry with the same key reports the post that exists rather than creating another one. Should that post have never been queued for publishing, the retry returns the same `scheduled: false` response above, so the resolution is again to `PATCH` or `DELETE` it. Retrying never queues publishing on your behalf, because a retry cannot tell "the task was never created" apart from "the task was created but the confirmation was lost", and guessing wrong would publish twice.
# Import posts from a social account (https://www.outstand.so/docs/import-posts-from-a-social-account)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Enqueues an import job to fetch and store existing posts from the connected social account platform. The job runs asynchronously - poll the returned import ID or listen for an `import.completed` / `import.failed` webhook event.
**Supported networks:** Bluesky, LinkedIn, Facebook, Instagram, Threads, TikTok, YouTube, Pinterest, Google Business, Reddit
**Note:** LinkedIn post imports are only supported for business (organization) accounts - personal LinkedIn accounts cannot import posts.
**Unsupported:** X (Twitter) - API tier restrictions prevent user timeline access.
**Billing:** Each successfully imported post counts as one `social_posts` usage unit, billed the same day the import runs.
## API Endpoint
`POST /v1/social-accounts/{id}/imports`
**Summary:** Import posts from a social account
Enqueues an import job to fetch and store existing posts from the connected social account platform. The job runs asynchronously - poll the returned import ID or listen for an `import.completed` / `import.failed` webhook event.
**Supported networks:** Bluesky, LinkedIn, Facebook, Instagram, Threads, TikTok, YouTube, Pinterest, Google Business
**Note:** LinkedIn post imports are only supported for business (organization) accounts - personal LinkedIn accounts cannot import posts.
**Unsupported:** X (Twitter) - API tier restrictions prevent user timeline access.
**Billing:** Each successfully imported post counts as one `social_posts` usage unit, billed the same day the import runs.
**Tags:** Social Accounts
## Parameters
- **id** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"since": {
"type": "string",
"format": "date-time",
"description": "Import posts published after this ISO 8601 timestamp",
"example": "2024-01-01T00:00:00Z"
},
"until": {
"type": "string",
"format": "date-time",
"description": "Import posts published before this ISO 8601 timestamp",
"example": "2025-01-01T00:00:00Z"
},
"limit": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 1000,
"description": "Maximum number of posts to import",
"example": 100
}
}
}
```
## Responses
### 202
Import job created and queued
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"orgId": {
"type": "string"
},
"socialAccountId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed",
"partial"
]
},
"since": {
"type": [
"string",
"null"
]
},
"until": {
"type": [
"string",
"null"
]
},
"limit": {
"type": [
"number",
"null"
]
},
"imported": {
"type": "number"
},
"skipped": {
"type": "number"
},
"failed": {
"type": "number"
},
"error": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"completedAt": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"orgId",
"socialAccountId",
"status",
"since",
"until",
"limit",
"imported",
"skipped",
"failed",
"error",
"createdAt",
"updatedAt",
"completedAt"
],
"description": "Import job status"
}
},
"required": [
"success",
"data"
]
}
```
### 400
Invalid request
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/social-accounts/{id}/imports \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"since": "2024-01-01T00:00:00Z",
"until": "2025-01-01T00:00:00Z",
"limit": 100
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}/imports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"since": "2024-01-01T00:00:00Z",
"until": "2025-01-01T00:00:00Z",
"limit": 100
})
});
const data = await response.json();
```
# List connected social accounts (https://www.outstand.so/docs/list-connected-social-accounts)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieve all social accounts connected to your organization. This endpoint returns all social media profiles that have been connected and authorized through OAuth flows.
**Use Cases:**
* View all connected social media accounts for your organization
* Check which platforms have accounts connected
* Verify account status and metadata
* Get account information for use in post creation endpoints
* Filter accounts by tenant ID, network, username, or unique platform ID
**Filtering:**
* `id` - Filter by encoded social account ID (exact match)
* `tenantId` - Filter by tenant ID for multi-tenant applications (exact match)
* `network` - Filter by social network platform, e.g. `x`, `linkedin`, `instagram` (exact match)
* `networkUniqueId` - Filter by the account's unique ID on the platform (exact match)
* `username` - Filter by username or handle (case-insensitive partial match)
* All filters can be combined for precise queries
**Pagination:**
* Use `limit` and `offset` query parameters to paginate results
* Default limit is 50, maximum is 100
**Tokens:** By default, OAuth tokens are never exposed. Pass `includeTokens=true` to include the account's `access_token` nested under a `tokens` object in each result. Only `access_token` is returned - refresh tokens and other sensitive fields remain hidden.
## API Endpoint
`GET /v1/social-accounts`
**Summary:** List connected social accounts
Retrieve all social accounts connected to your organization. This endpoint returns all social media profiles that have been connected and authorized through OAuth flows.
**Use Cases:**
- View all connected social media accounts for your organization
- Check which platforms have accounts connected
- Verify account status and metadata
- Get account information for use in post creation endpoints
- Filter accounts by tenant ID, network, username, or unique platform ID
**Filtering:**
- `id` - Filter by encoded social account ID (exact match)
- `tenantId` - Filter by tenant ID for multi-tenant applications (exact match)
- `network` - Filter by social network platform, e.g. `x`, `linkedin`, `instagram` (exact match)
- `networkUniqueId` - Filter by the account's unique ID on the platform (exact match)
- `username` - Filter by username or handle (case-insensitive partial match)
- All filters can be combined for precise queries
**Pagination:**
- Use `limit` and `offset` query parameters to paginate results
- Default limit is 50, maximum is 100
- `total` is the number of accounts matching your filters across all pages; `count` is the number returned in this page
- Results are always ordered by connection order (oldest connected first), so paging through every offset returns each account exactly once. Accounts connected while you are paginating are appended after the last page rather than shifting the pages you already fetched
**Tokens:** By default, OAuth tokens are never exposed. Pass `includeTokens=true` to include the account's `access_token` nested under a `tokens` object in each result. Only `access_token` is returned - refresh tokens and other sensitive fields remain hidden.
**Tags:** Social Accounts
## Parameters
- **id** (query: string): Filter by social account ID (exact match, encoded ID as returned by the API)
- **tenantId** (query: string): Filter by tenant ID (exact match)
- **network** (query: string): Filter by social network platform (exact match, e.g. 'x', 'linkedin', 'instagram', 'threads', 'bluesky', 'facebook', 'tiktok', 'youtube', 'pinterest', 'vimeo', 'reddit')
- **networkUniqueId** (query: string): Filter by the account's unique identifier on the social network platform (exact match)
- **username** (query: string): Filter by username or handle (case-insensitive partial match)
- **limit** (query: number): Maximum number of results to return (1-100)
- **offset** (query: number): Number of results to skip for pagination
- **includeTokens** (query: string): When true, include the account's access_token (from network_data) under a `tokens` object in each result. Defaults to false; tokens are never returned otherwise.
## Responses
### 200
List of social accounts retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the social account",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"description": "Organization ID that owns this social account",
"example": "org_abc123"
},
"tenant_id": {
"type": [
"string",
"null"
],
"description": "Your own tenant identifier for this account, as supplied when it was connected. Null when the account was connected without one - such an account is not matched by the `tenantId` filter.",
"example": "tenant_123"
},
"nickname": {
"type": "string",
"description": "User-friendly nickname for the social account",
"example": "My Company Twitter"
},
"network": {
"type": "string",
"description": "Social network platform (e.g., 'x', 'linkedin', 'instagram')",
"example": "x"
},
"username": {
"type": "string",
"description": "Username or handle for the social account",
"example": "@mycompany"
},
"profile_picture_url": {
"type": [
"string",
"null"
],
"description": "URL to the profile picture for the social account",
"example": "https://example.com/profile.jpg"
},
"network_unique_id": {
"type": "string",
"description": "Unique identifier for the account on the social network platform",
"example": "123456789"
},
"network_webhook_reference_id": {
"type": [
"string",
"null"
],
"description": "The id this network uses to identify the account in webhook payloads, when it differs from network_unique_id. Null when the platform uses the same id for both. Used by: Instagram.",
"example": "17841469709963147"
},
"customer_social_network_id": {
"type": "number",
"description": "ID of the customer social network configuration used to connect this account",
"example": 5
},
"accountType": {
"type": "string",
"description": "Type of account: 'personal', 'organization', or 'page'",
"example": "organization"
},
"isActive": {
"type": [
"number",
"null"
],
"description": "Whether the account is active (1) or inactive (0)",
"example": 1
},
"createdAt": {
"type": [
"string",
"null"
],
"description": "ISO 8601 timestamp when the account was connected",
"example": "2025-01-15T10:30:00Z"
},
"tokens": {
"type": "object",
"properties": {
"access_token": {
"type": [
"string",
"null"
],
"description": "OAuth access token for the account. Only present when the request included includeTokens=true.",
"example": "ya29.a0Af..."
}
},
"required": [
"access_token"
],
"description": "Sensitive OAuth tokens. Only included when includeTokens=true is passed."
}
},
"required": [
"id",
"orgId",
"tenant_id",
"nickname",
"network",
"username",
"profile_picture_url",
"network_unique_id",
"customer_social_network_id",
"accountType",
"isActive",
"createdAt"
],
"description": "Social account information"
}
},
"count": {
"type": "number",
"description": "Number of social accounts returned in this page",
"example": 3
},
"total": {
"type": "number",
"description": "Total number of social accounts matching the filters, across all pages",
"example": 292
},
"limit": {
"type": "number",
"description": "Maximum number of results per page",
"example": 50
},
"offset": {
"type": "number",
"description": "Number of results to skip",
"example": 0
}
},
"required": [
"success",
"data",
"count",
"total",
"limit",
"offset"
],
"description": "Successful list operation response"
}
```
### 400
Invalid query parameters
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Session expired or invalid"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List connected social networks (https://www.outstand.so/docs/list-connected-social-networks)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Retrieve all social networks configured for your organization. This endpoint returns all stored social networks across all supported platforms.
**Security:** For security reasons, the client\_secret field is never included in API responses, even though it is stored securely in the database. Only the client\_key (public identifier), network type, and metadata are returned.
**Use Cases:**
* Audit which social networks are configured
* Check if social networks are configured before attempting to connect accounts
* Verify which platforms are available for posting
* Monitor social network creation timestamps
## API Endpoint
`GET /v1/social-networks`
**Summary:** List connected social networks
Retrieve all social networks configured for your organization. This endpoint returns all stored social networks across all supported platforms.
**Security:** For security reasons, the client_secret field is never included in API responses, even though it is stored securely in the database. Only the client_key (public identifier), network type, and metadata are returned.
**Use Cases:**
- Audit which social networks are configured
- Check if social networks are configured before attempting to connect accounts
- Verify which platforms are available for posting
- Monitor social network creation timestamps
**Tags:** Customer Social Networks
## Responses
### 200
List of networks retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"oauth_callback_url": {
"type": [
"string",
"null"
],
"maxLength": 2048,
"description": "YouTube BYOK only. Exact customer-hosted HTTPS callback registered with Google. Omit to preserve the default; PATCH null clears it. Requires exactly one YouTube credential configuration.",
"example": "https://auth.example.com/youtube/callback"
},
"id": {
"type": "string",
"description": "Unique identifier for the network",
"example": "abc123"
},
"network": {
"type": "string",
"enum": [
"threads",
"bluesky",
"x",
"linkedin",
"youtube",
"instagram",
"facebook",
"tiktok",
"pinterest",
"google_business",
"vimeo",
"reddit"
],
"description": "Social network platform identifier. Each platform requires specific OAuth credentials obtained from their respective developer portals. Refer to our configuration guides for detailed instructions on obtaining credentials for each platform.",
"example": "x"
},
"client_key": {
"type": "string",
"description": "Client key for the social network",
"example": "your_client_key_here"
},
"createdAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was created",
"example": "2025-01-15T10:30:00Z"
},
"updatedAt": {
"type": "string",
"description": "ISO 8601 timestamp when the credential was last updated",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"oauth_callback_url",
"id",
"network",
"client_key",
"createdAt",
"updatedAt"
],
"description": "Customer social network response (client_secret is never included)"
}
},
"count": {
"type": "number",
"description": "Total number of networks returned",
"example": 3
}
},
"required": [
"success",
"data",
"count"
],
"description": "Successful list operation response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-networks \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-networks', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List import jobs for a social account (https://www.outstand.so/docs/list-import-jobs-for-a-social-account)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Returns all import jobs for the specified social account, ordered by most recent first.
## API Endpoint
`GET /v1/social-accounts/{id}/imports`
**Summary:** List import jobs for a social account
Returns all import jobs for the specified social account, ordered by most recent first.
**Tags:** Social Accounts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Import jobs retrieved
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"orgId": {
"type": "string"
},
"socialAccountId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed",
"partial"
]
},
"since": {
"type": [
"string",
"null"
]
},
"until": {
"type": [
"string",
"null"
]
},
"limit": {
"type": [
"number",
"null"
]
},
"imported": {
"type": "number"
},
"skipped": {
"type": "number"
},
"failed": {
"type": "number"
},
"error": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"completedAt": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"orgId",
"socialAccountId",
"status",
"since",
"until",
"limit",
"imported",
"skipped",
"failed",
"error",
"createdAt",
"updatedAt",
"completedAt"
],
"description": "Import job status"
}
},
"count": {
"type": "number"
}
},
"required": [
"success",
"data",
"count"
]
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Resource not found response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/social-accounts/{id}/imports \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/social-accounts/{id}/imports', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List media files (https://www.outstand.so/docs/list-media-files)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Get a paginated list of all active (confirmed) media files for your organization. Only non-expired files are returned. Use `limit` and `offset` query parameters for pagination. Each file includes its public URL, content type, size, and expiration date. Files expire 60 days after upload. Use the returned media IDs when creating posts to attach media content.
## API Endpoint
`GET /v1/media`
**Summary:** List media files
Get a paginated list of all active (confirmed) media files for your organization. Only non-expired files are returned. Use `limit` and `offset` query parameters for pagination. Each file includes its public URL, content type, size, and expiration date. Files expire 60 days after upload. Use the returned media IDs when creating posts to attach media content.
**Tags:** Media
## Parameters
- **limit** (query: string): Number of media files per page (1-100)
- **offset** (query: string): Pagination offset
## Responses
### 200
Media files retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the media file",
"example": "9dyJS"
},
"filename": {
"type": "string",
"description": "Original filename of the media file",
"example": "product-image.jpg"
},
"url": {
"type": "string",
"format": "uri",
"description": "Publicly accessible URL for the media file",
"example": "https://media.outstand.so/org_abc123/550e8400-e29b-41d4-a716-446655440000/product-image.jpg"
},
"content_type": {
"type": [
"string",
"null"
],
"description": "MIME type of the media file",
"example": "image/jpeg"
},
"size": {
"type": [
"number",
"null"
],
"description": "File size in bytes",
"example": 1024000
},
"status": {
"type": "string",
"enum": [
"pending",
"active",
"deleted"
],
"description": "Current status of the media file",
"example": "active"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file was uploaded",
"example": "2025-01-15T10:30:00Z"
},
"expires_at": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp when the file will expire (60 days from creation)",
"example": "2025-03-16T10:30:00Z"
}
},
"required": [
"id",
"filename",
"url",
"content_type",
"size",
"status",
"created_at",
"expires_at"
],
"description": "Media file object"
}
},
"pagination": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"example": 50
},
"offset": {
"type": "number",
"example": 0
},
"count": {
"type": "number",
"description": "Number of media files returned",
"example": 10
}
},
"required": [
"limit",
"offset",
"count"
]
}
},
"required": [
"success",
"data",
"pagination"
],
"description": "Response containing list of media files"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"message": {
"type": "string",
"example": "Failed to generate upload URL"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/media \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/media', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List pending replies (https://www.outstand.so/docs/list-pending-replies)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
List replies that are pending approval on a Threads post managed by Outstand (Threads' reply-approvals feature). Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account). Cursor-paginated via `limit` and `after`.
## API Endpoint
`GET /v1/threads/posts/{postId}/pending-replies`
**Summary:** List pending replies
List replies that are pending approval on a Threads post managed by Outstand (Threads' reply-approvals feature). Use `platform_post_id` or `account_username` to select which connected account's credentials to use (optional when the post was published to only one account). Cursor-paginated via `limit` and `after`.
**Tags:** Threads, Replies
## Parameters
- **platform_post_id** (query: string): The platform-specific post ID of the account whose credentials should be used. Optional when the post was published to only one account.
- **account_username** (query: string): The username or nickname of the connected account whose credentials should be used. Optional when the post was published to only one account.
- **limit** (query: integer): Maximum number of pending replies to return (1-100). Defaults to 25.
- **after** (query: string): Pagination cursor returned in a previous response's paging.cursors.after.
- **postId** (path: string) [required]
## Responses
### 200
Pending replies retrieved
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Threads reply media ID",
"example": "17900000000000000"
},
"text": {
"type": "string",
"description": "Reply text",
"example": "Great post!"
},
"username": {
"type": "string",
"description": "Author username",
"example": "someuser"
},
"timestamp": {
"type": "string",
"description": "ISO 8601 creation timestamp"
},
"permalink": {
"type": "string",
"description": "Permalink to the reply"
},
"reply_audience": {
"type": "string",
"description": "Reply audience of the reply"
},
"reply_approval_status": {
"type": "string",
"description": "Approval status of the pending reply"
}
},
"required": [
"id"
],
"description": "A pending reply"
}
},
"paging": {
"description": "Cursor pagination object returned by Threads, if any."
}
},
"required": [
"success",
"data"
],
"description": "Pending replies response"
}
```
### 400
Invalid request
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post or matching social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Post not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/threads/posts/{postId}/pending-replies \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/threads/posts/{postId}/pending-replies', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List Pinterest boards (https://www.outstand.so/docs/list-pinterest-boards)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
List every board the connected Pinterest account owns. Results are paginated by Pinterest's `bookmark` parameter under the hood and aggregated into a single response.
**Use case:** populate a board picker before publishing a pin. Pass the chosen board's `id` as `pinterestConfiguration.board_id` when calling `POST /v1/posts/`.
## API Endpoint
`GET /v1/pinterest/accounts/{id}/boards`
**Summary:** List Pinterest boards
List every board the connected Pinterest account owns. Results are paginated by Pinterest's `bookmark` parameter under the hood and aggregated into a single response.
**Use case:** populate a board picker before publishing a pin. Pass the chosen board's `id` as `pinterest.board_id` when calling `POST /v1/posts/`.
**Tags:** Pinterest
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Boards retrieved
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Pinterest board ID",
"example": "987654321098765432"
},
"name": {
"type": "string",
"description": "Board name",
"example": "Summer Outfits"
},
"description": {
"type": "string",
"description": "Board description",
"example": "My favourite summer looks"
},
"pin_count": {
"type": "number",
"description": "Number of pins on the board",
"example": 42
},
"privacy": {
"type": "string",
"enum": [
"PUBLIC",
"PROTECTED",
"SECRET"
],
"description": "Board privacy",
"example": "PUBLIC"
},
"owner": {
"type": "object",
"properties": {
"username": {
"type": "string",
"example": "myaccount"
}
},
"required": [
"username"
],
"description": "Board owner"
},
"created_at": {
"type": "string",
"description": "ISO 8601 creation timestamp"
}
},
"required": [
"id",
"name",
"privacy"
],
"description": "A Pinterest board"
}
},
"count": {
"type": "number",
"description": "Number of boards returned",
"example": 12
}
},
"required": [
"success",
"data",
"count"
],
"description": "List of Pinterest boards"
}
```
### 404
Social account not found
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Social account not found"
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/pinterest/accounts/{id}/boards \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/pinterest/accounts/{id}/boards', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# List posts (https://www.outstand.so/docs/list-posts)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Get a paginated list of posts for the current organization with optional filters. You can filter by social account ID, creation date range, or scheduled date range. Results include post content, containers, scheduling status, and publishing state. Use `limit` and `offset` query parameters for pagination. Each returned post includes its unique ID, which can be used with GET /v1/posts/:id for full details or DELETE /v1/posts/:id to cancel a scheduled post.
## API Endpoint
`GET /v1/posts`
**Summary:** List posts
Get a paginated list of posts for the current organization with optional filters. Results include post content, containers, scheduling status, and publishing state. Each returned post includes its unique ID, which can be used with GET /v1/posts/:id for full details or DELETE /v1/posts/:id to cancel a scheduled post.
**Filtering:**
- `tenant_id` - Filter by tenant ID for multi-tenant applications (exact match). Posts carry no tenant of their own, so a post matches when at least one of its target social accounts has that tenant ID. `tenantId` is accepted as an alias for parity with GET /v1/social-accounts.
- `social_account_id` - Filter by social account ID (posts targeting that account). Accepts the encoded ID returned by the API, or a raw numeric ID.
- `created_after` / `created_before` - Creation timestamp range (ISO 8601)
- `scheduled_after` / `scheduled_before` - Scheduled timestamp range (ISO 8601)
- All filters combine with AND.
Note that filters select *posts*: a matching post is returned with its full `socialAccounts` array, so a post fanned out across several tenants will list all of its accounts. When `tenant_id` and `social_account_id` are combined they must be satisfied by the same account, so pairing a tenant with an account outside it returns no results rather than matching through a sibling account.
**Pagination:** use `limit` and `offset`. `pagination.total` is the total number of posts matching the filters across all pages; `pagination.count` is the number of posts in the current page.
**Tags:** Posts
## Parameters
- **social_account_id** (query: string): Filter by social account ID. Accepts the encoded ID returned by the API, or a raw numeric ID.
- **tenant_id** (query: string): Filter by tenant ID for multi-tenant applications (exact match). Returns posts targeting at least one social account with this tenant ID.
- **tenantId** (query: string): Alias for `tenant_id`, accepted for parity with GET /v1/social-accounts. If both are provided, `tenant_id` wins.
- **created_after** (query: string): Filter posts created after this timestamp
- **created_before** (query: string): Filter posts created before this timestamp
- **scheduled_after** (query: string): Filter posts scheduled after this timestamp
- **scheduled_before** (query: string): Filter posts scheduled before this timestamp
- **limit** (query: string): Number of posts per page (1-100)
- **offset** (query: string): Pagination offset
## Responses
### 200
Posts retrieved successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"posts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"example": "abc123"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"scheduledAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isDraft": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"socialAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the social account",
"example": "9dyJS"
},
"nickname": {
"type": "string",
"description": "User-defined nickname for this social account"
},
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'facebook', 'x', 'instagram', 'linkedin', 'threads', 'youtube', 'tiktok')"
},
"username": {
"type": "string",
"description": "Username or handle on the social network"
},
"status": {
"type": "string",
"enum": [
"pending",
"published",
"failed",
"deleted"
],
"description": "Publishing status for this account. 'pending': awaiting publish (post created or scheduled), 'published': successfully published to this account, 'failed': publishing failed (check error field), 'deleted': post was deleted from the social network platform. Note: A post can partially succeed - some accounts may publish while others fail.",
"example": "published"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message if publishing failed. Contains the platform-specific error message. Common errors include: expired access tokens, rate limits, invalid media format, content policy violations. Null if status is not 'failed'.",
"example": null
},
"platformPostId": {
"type": [
"string",
"null"
],
"description": "The platform-specific post ID after successful publish. Use this for analytics, fetching comments, or deep-linking to the post on the platform. Null if not yet published or if publishing failed.",
"example": "123456789"
},
"platformPostUrl": {
"type": [
"string",
"null"
],
"description": "The public URL of the post on the social network. Currently populated for Instagram only; null for other networks or for posts created before this feature.",
"example": "https://www.instagram.com/p/DAbCdEfGhIj/"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when the post was successfully published to this account. Null if pending or failed. Each account has its own publishedAt since they are processed sequentially.",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"id",
"nickname",
"network",
"username",
"status",
"error",
"platformPostId",
"platformPostUrl",
"publishedAt"
]
}
},
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "8xKmL"
},
"content": {
"type": "string"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": [
"url",
"filename"
]
}
}
},
"required": [
"id",
"content",
"media"
]
}
}
},
"required": [
"id",
"orgId",
"publishedAt",
"scheduledAt",
"isDraft",
"createdAt",
"socialAccounts",
"containers"
]
},
"description": "Array of posts (kept for backward compatibility)"
},
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "9dyJS"
},
"orgId": {
"type": "string",
"example": "abc123"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"scheduledAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isDraft": {
"type": "boolean"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"socialAccounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the social account",
"example": "9dyJS"
},
"nickname": {
"type": "string",
"description": "User-defined nickname for this social account"
},
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'facebook', 'x', 'instagram', 'linkedin', 'threads', 'youtube', 'tiktok')"
},
"username": {
"type": "string",
"description": "Username or handle on the social network"
},
"status": {
"type": "string",
"enum": [
"pending",
"published",
"failed",
"deleted"
],
"description": "Publishing status for this account. 'pending': awaiting publish (post created or scheduled), 'published': successfully published to this account, 'failed': publishing failed (check error field), 'deleted': post was deleted from the social network platform. Note: A post can partially succeed - some accounts may publish while others fail.",
"example": "published"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message if publishing failed. Contains the platform-specific error message. Common errors include: expired access tokens, rate limits, invalid media format, content policy violations. Null if status is not 'failed'.",
"example": null
},
"platformPostId": {
"type": [
"string",
"null"
],
"description": "The platform-specific post ID after successful publish. Use this for analytics, fetching comments, or deep-linking to the post on the platform. Null if not yet published or if publishing failed.",
"example": "123456789"
},
"platformPostUrl": {
"type": [
"string",
"null"
],
"description": "The public URL of the post on the social network. Currently populated for Instagram only; null for other networks or for posts created before this feature.",
"example": "https://www.instagram.com/p/DAbCdEfGhIj/"
},
"publishedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "ISO 8601 timestamp when the post was successfully published to this account. Null if pending or failed. Each account has its own publishedAt since they are processed sequentially.",
"example": "2025-01-15T10:30:00Z"
}
},
"required": [
"id",
"nickname",
"network",
"username",
"status",
"error",
"platformPostId",
"platformPostUrl",
"publishedAt"
]
}
},
"containers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "8xKmL"
},
"content": {
"type": "string"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"required": [
"url",
"filename"
]
}
}
},
"required": [
"id",
"content",
"media"
]
}
}
},
"required": [
"id",
"orgId",
"publishedAt",
"scheduledAt",
"isDraft",
"createdAt",
"socialAccounts",
"containers"
]
},
"description": "Array of posts (for UI package compatibility)"
},
"pagination": {
"type": "object",
"properties": {
"limit": {
"type": "number"
},
"offset": {
"type": "number"
},
"total": {
"type": "number",
"description": "Total number of posts matching the filters, across all pages"
},
"count": {
"type": "number",
"description": "Number of posts in this page"
}
},
"required": [
"limit",
"offset",
"total",
"count"
]
}
},
"required": [
"success",
"posts",
"pagination"
]
}
```
### 400
Invalid query parameters
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X GET https://api.outstand.so/v1/posts \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Post Lifecycle (https://www.outstand.so/docs/post-lifecycle)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
Understanding how posts move through different states in Outstand is crucial for building robust integrations. This guide explains the post lifecycle, status fields, timing expectations, and where to find error information.
## Post States
Every post in Outstand goes through a lifecycle from creation to publication. The state is tracked both at the **post level** and at the **per-account level**.
### Post-Level Status
At the post level, the `publishedAt` field indicates whether any account has successfully published:
| Field | Type | Description |
| ------------- | ------------------ | --------------------------------------------------------------------------------------------------- |
| `publishedAt` | `datetime \| null` | Set when the first social account successfully publishes. `null` if no accounts have published yet. |
| `scheduledAt` | `datetime \| null` | When the post is scheduled to publish. `null` for immediate publishing. |
### Per-Account Status
Each social account attached to a post has its own publishing status:
| Field | Type | Description |
| ---------------- | -------------------------------------- | ------------------------------------------------------- |
| `status` | `'pending' \| 'published' \| 'failed'` | Current publishing state for this account. |
| `error` | `string \| null` | Error message if publishing failed. `null` otherwise. |
| `platformPostId` | `string \| null` | The platform-specific post ID after successful publish. |
| `publishedAt` | `datetime \| null` | When this account successfully published. |
## State Transitions
### pending
When a post is created and scheduled, each social account starts in the `pending` state. The publishing worker will attempt to publish to each account when the scheduled time arrives (or immediately if no `scheduledAt` is specified).
### published
When publishing succeeds for a social account:
* `status` is set to `published`
* `platformPostId` is populated with the platform-specific ID (e.g., Twitter post ID, Instagram media ID)
* `publishedAt` is set to the current timestamp
* `error` is set to `null`
### failed
When publishing fails for a social account:
* `status` is set to `failed`
* `error` contains the error message from the platform
* `platformPostId` remains `null`
* `publishedAt` remains `null`
## Timing Expectations
| Scenario | Expected Timing |
| ------------------------------------ | ----------------------------------------------------- |
| Post creation | Immediate response with post ID |
| Immediate publish (no `scheduledAt`) | Publishing starts within seconds |
| Scheduled publish | Publishing starts at the scheduled time (±30 seconds) |
| Status visibility via API | Available immediately after publishing completes |
| Webhook delivery | Sent within seconds of publishing completion |
**Note**: Publishing to a social account typically takes 1-10 seconds depending on the platform and whether media upload is required. For posts with multiple accounts, each account is processed sequentially.
## Where to Find Errors
Outstand provides two ways to get error information:
### 1. API Response (Polling)
Use `GET /v1/posts/{id}` to check the status of a post. Each social account in the response includes `status`, `error`, `platformPostId`, and `publishedAt` fields:
```json
{
"success": true,
"post": {
"id": "9dyJS",
"publishedAt": "2025-01-15T10:30:00Z",
"socialAccounts": [
{
"id": "abc123",
"network": "facebook",
"username": "MyPage",
"status": "failed",
"error": "Failed to upload Facebook photo from buffer: 400 - (#100) Invalid image",
"platformPostId": null,
"publishedAt": null
},
{
"id": "def456",
"network": "x",
"username": "myaccount",
"status": "published",
"error": null,
"platformPostId": "1234567890123456789",
"publishedAt": "2025-01-15T10:30:02Z"
}
]
}
}
```
### 2. Webhooks (Push)
If you've configured webhooks, you'll receive real-time notifications when publishing completes:
**`post.published`** - Sent when at least one account succeeds:
```json
{
"event": "post.published",
"timestamp": "2025-01-15T10:30:05Z",
"data": {
"postId": "9dyJS",
"socialAccounts": [
{
"network": "x",
"username": "myaccount",
"platformPostId": "1234567890123456789"
},
{
"network": "facebook",
"username": "MyPage",
"error": "Failed to upload..."
}
]
}
}
```
**`post.error`** - Sent when ALL accounts fail:
```json
{
"event": "post.error",
"timestamp": "2025-01-15T10:30:05Z",
"data": {
"postId": "9dyJS",
"socialAccounts": [
{
"network": "facebook",
"username": "MyPage",
"error": "Access token has expired"
}
]
}
}
```
See the [Webhooks documentation](/webhooks) for setup instructions.
## Best Practices
1. **Use webhooks for real-time updates**: Instead of polling, configure webhooks to receive immediate notifications when publishing completes or fails.
2. **Check per-account status**: A post can partially succeed - some accounts may publish while others fail. Always check the `status` field for each social account.
3. **Handle retries**: If publishing fails due to a transient error (rate limit, temporary outage), you can delete the failed post and create a new one.
4. **Store platform IDs**: After successful publishing, store the `platformPostId` returned for each account. You'll need this for analytics, comments, and other platform-specific operations.
5. **Monitor for token expiration**: Common errors like "Access token has expired" indicate the need to reconnect the social account.
# Publish a comment (https://www.outstand.so/docs/publish-a-comment)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Create a reply (comment) on a post that was published through Outstand. You can target a specific social account by providing either the platform\_post\_id (the native post ID on the social network) or the account\_username of the connected account. Replies can be made to the root post or to another account's reply/comment under your own post. Note: We currently do not support interacting with posts that were not published through Outstand. The post must have been published (not just scheduled) to the target account. Comments are not supported on every network - TikTok, Pinterest, and Google Business Profile do not expose a comments API, and YouTube comments are not yet supported; these networks will return an error.
## API Endpoint
`POST /v1/posts/{id}/replies`
**Summary:** Publish a comment
Create a reply (comment) on a post that was published through Outstand. You can target a specific social account by providing either the platform_post_id (the native post ID on the social network) or the account_username of the connected account. Replies can be made to the root post or to another account's reply/comment under your own post. To thread the reply under a specific comment instead of the post, pass parent_comment_id (the comment's platform ID/URN) - supported on Facebook, Instagram, LinkedIn, and Threads. Facebook, Instagram, and LinkedIn allow a single reply tier (replies to replies collapse onto the top-level comment), while Threads preserves the nesting at any depth. Note: We currently do not support interacting with posts that were not published through Outstand. The post must have been published (not just scheduled) to the target account. Comments are not supported on every network - TikTok, Pinterest, and Google Business Profile do not expose a comments API, and YouTube comments are not yet supported; these networks will return an error.
**Tags:** Posts, Comments
## Parameters
- **id** (path: string) [required]
## Request Body
```json
{
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The content of the reply",
"example": "Great post! I agree with you."
},
"platform_post_id": {
"type": "string",
"description": "The platform-specific post ID to reply to (e.g., X post ID, Threads post ID, etc.)",
"example": "123456789"
},
"account_username": {
"type": "string",
"description": "The username or nickname of the social account to reply from",
"example": "mycompany"
},
"parent_comment_id": {
"type": "string",
"description": "The platform-specific ID (or URN) of a comment to reply to, threading the reply under that comment instead of the post. Supported on Facebook, Instagram, LinkedIn, and Threads. Facebook, Instagram, and LinkedIn allow a single reply tier, so replies to replies are flattened onto the top-level comment; Threads nests them for real, so you can reply to a reply at any depth.",
"example": "17851234567890123"
}
},
"required": [
"content"
]
}
```
## Responses
### 200
Reply published successfully
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"reply_id": {
"type": "string",
"description": "The platform-specific ID of the published reply",
"example": "123456789"
}
},
"required": [
"success",
"reply_id"
]
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/posts/{id}/replies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Great post! I agree with you.",
"platform_post_id": "123456789",
"account_username": "mycompany",
"parent_comment_id": "17851234567890123"
}'
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/replies', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"content": "Great post! I agree with you.",
"platform_post_id": "123456789",
"account_username": "mycompany",
"parent_comment_id": "17851234567890123"
})
});
const data = await response.json();
```
# Repost a post to social networks (https://www.outstand.so/docs/repost-a-post-to-social-networks)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
Repost (reshare) a published post to all remote social network platforms it was published to. Optionally pass a 'quote' to add commentary on top of the repost where the network supports it (X quote tweet, Bluesky quote post, LinkedIn reshare with commentary, Threads quote post) - otherwise a plain repost/reshare is created. Returns per-account results. Networks that do not support reposting via API (Facebook, Instagram, Pinterest, TikTok, Google Business, Vimeo, Reddit, YouTube) will appear with status='failed'. At least one successful repost returns HTTP 200 with success=true.
## API Endpoint
`POST /v1/posts/{id}/repost`
**Summary:** Repost a post to social networks
Repost (reshare) a published post to all remote social network platforms it was published to. Optionally pass a 'quote' to add commentary on top of the repost where the network supports it (X quote tweet, Bluesky quote post, LinkedIn reshare with commentary, Threads quote post) - otherwise a plain repost/reshare is created. Returns per-account results. Networks that do not support reposting via API (Facebook, Instagram, Pinterest, TikTok, Google Business, Vimeo, YouTube) will appear with status='failed'. At least one successful repost returns HTTP 200 with success=true.
**Tags:** Posts
## Parameters
- **id** (path: string) [required]
## Responses
### 200
Repost attempted. Check the 'results' array for per-account status. 'success' is true if at least one network was reposted to.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "True if at least one social network account was successfully reposted to. False if all accounts failed.",
"example": true
},
"results": {
"type": "array",
"items": {
"type": "object",
"properties": {
"network": {
"type": "string",
"description": "Social network identifier (e.g., 'x', 'linkedin', 'bluesky')",
"example": "bluesky"
},
"username": {
"type": "string",
"description": "Username or handle on the social network",
"example": "mycompany"
},
"platform_post_id": {
"type": [
"string",
"null"
],
"description": "The platform-specific post ID of the original post that was reposted",
"example": "at://did:plc:abc123/app.bsky.feed.post/xyz"
},
"status": {
"type": "string",
"enum": [
"reposted",
"failed"
],
"description": "'reposted': the repost/reshare was created successfully. 'failed': the repost failed, or the network does not support reposting via API (see error field).",
"example": "reposted"
},
"repost_id": {
"type": [
"string",
"null"
],
"description": "The platform-specific ID of the newly created repost/quote record. Null when status is 'failed'.",
"example": "at://did:plc:abc123/app.bsky.feed.repost/def456"
},
"error": {
"type": [
"string",
"null"
],
"description": "Error message when status is 'failed'. Examples: 'Reposting is not supported by Instagram's API', 'X API error 403: Forbidden'.",
"example": null
}
},
"required": [
"network",
"username",
"platform_post_id",
"status",
"repost_id",
"error"
]
},
"description": "Per-account repost results. Networks without a repost API (Facebook, Instagram, Pinterest, TikTok, Google Business, Vimeo, YouTube) will appear here with status='failed'."
}
},
"required": [
"success",
"results"
]
}
```
### 400
Post has not been published to any social network yet.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 403
Unauthorized - post does not belong to your organization.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 404
Post not found.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Invalid payload"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_key_reuse"
},
"details": {
"example": {
"content": {
"_errors": [
"Required"
]
}
}
}
},
"required": [
"success",
"error"
],
"description": "Error response"
}
```
### 500
Internal server error.
```json
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": false
},
"error": {
"type": "string",
"example": "Internal server error"
},
"code": {
"type": "string",
"description": "Stable machine-readable error code, when one applies",
"example": "idempotency_recovery_failed"
},
"message": {
"type": "string",
"example": "Database connection failed"
}
},
"required": [
"success",
"error"
],
"description": "Internal server error response"
}
```
## Example Request
```bash
curl -X POST https://api.outstand.so/v1/posts/{id}/repost \
-H "Authorization: Bearer YOUR_API_KEY"
```
```typescript
const response = await fetch('https://api.outstand.so/v1/posts/{id}/repost', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
},
});
const data = await response.json();
```
# Set usage limits (https://www.outstand.so/docs/set-usage-limits)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
> Endpoint coming soon
Set the usage limits for the current organization.
```json
{
"accounts_under_management": 100,
"posts_per_account_per_month": 1000
}
```
# TikTok Direct Post Audit Guide (https://www.outstand.so/docs/tiktok-audit)
> Base URL: https://api.outstand.so | Auth: Bearer token in Authorization header
This guide walks you through passing TikTok's **Content Posting API audit** for Direct Post, so you can use your own TikTok API credentials with Outstand ([BYOK](/configurations/tiktok)) without the unaudited restrictions.
Everything here is derived from TikTok's [Content Sharing Guidelines - Direct Post API Developer Guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines#direct_post_api_-_developer_guidelines). That page is the checklist reviewers work from. Read it in full before you submit - this guide tells you how to satisfy it, not what it replaces.
## Why the audit matters
Until your API client passes the audit, TikTok applies two hard restrictions:
| Restriction | Effect | Error code |
| -------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Private-only posting | Every Direct Post is forced to `SELF_ONLY`, regardless of what you send | `unaudited_client_can_only_post_to_private_accounts` |
| Active creator cap | At most **5 distinct creators** can publish through your app in a rolling 24 hours | `reached_active_user_cap` |
Passing the audit lifts the private-only restriction and raises the creator cap to a number based on the usage estimates in your application.
> **You can ship without the audit.** `MEDIA_UPLOAD` (the Outstand default) delivers the media to the creator's TikTok inbox as a draft. It is not subject to the creator cap and has no visibility restriction, because the creator publishes it themselves from the TikTok app. See [Post Modes](/configurations/tiktok#post-modes). Only apply for the audit if you genuinely need `DIRECT_POST`.
If you landed here because Direct Posts started failing with `reached_active_user_cap` on Outstand's managed TikTok credentials, read [TikTok - Direct Post Active Creator Cap](/known-issues/tiktok-active-user-cap) first. It covers the immediate workarounds; getting your own app audited is the durable fix.
## Before you apply
TikTok will reject the application outright if any of these are missing, before a human ever looks at your UI.
* [ ] The app is in **production** (not sandbox) and has been tested end to end.
* [ ] `video.publish` is enabled on the app. Direct Post is unusable without it - `video.upload` only covers inbox drafts.
* [ ] Your **app name, website URL and redirect URI all reference the same brand**. If you are using Outstand's callback directly, this is the most common rejection reason - use the [proxy callback](/configurations/tiktok#proxy-callback-implementation) so TikTok sees your own domain.
* [ ] Your **Terms of Service** and **Privacy Policy** URLs are live, HTTPS, and reachable without a login.
* [ ] If you deliver media with `PULL_FROM_URL`, the hosting domain is **verified under URL Properties** in the developer portal. Unverified domains fail later with `url_ownership_unverified`.
* [ ] Your `client_secret` is server-side only and is not in any public repository.
* [ ] A demo video is recorded and hosted somewhere TikTok's reviewers can open without an account (unlisted YouTube, Loom, or a direct MP4 link all work).
## Audit templated answers
TikTok's audit form asks you to describe your integration in free text. The exact field labels change from time to time, so treat these as **answer templates keyed to the question being asked**, not as a literal form. Replace everything in `{{ braces }}`.
Reviewers are looking for two things in every answer: that real creators consent to each post, and that you are not bulk-reposting content scraped from elsewhere.
### "Describe your product and how it uses the Content Posting API"
```text
{{ Product name }} is a {{ social media scheduling / creator marketing / brand
publishing }} tool used by {{ audience: e.g. brands, agencies and individual
creators }}. Creators connect their own TikTok account via TikTok Login and use
{{ Product name }} to prepare and publish their own original content.
We use the Content Posting API in two modes:
- Direct Post (/v2/post/publish/video/init/ and /v2/post/publish/content/init/)
when the creator has completed the full post form in our composer - caption,
visibility, interaction settings and content disclosure - and has explicitly
pressed Post.
- Inbox upload (/v2/post/publish/inbox/video/init/) when the creator prefers to
finish the post inside the TikTok app.
Before rendering the post form we call /v2/post/publish/creator_info/query/ to
fetch the creator's nickname, avatar, privacy_level_options,
max_video_post_duration_sec and their comment/duet/stitch settings, and we build
the form entirely from that response. After init we poll
/v2/post/publish/status/fetch/ until we reach a terminal status and report the
real outcome back to the creator.
```
### "Who are your users and how do they obtain the content they post?"
```text
Our users are the creators and the brands who own the accounts they connect.
Content is uploaded by the user from their own device or their own media library
inside {{ Product name }}. We do not import, scrape or repost content from other
platforms, and we do not operate the connected accounts on the users' behalf.
Nothing is ever sent to TikTok without the account owner completing the post
form and pressing Post.
```
### "How many users do you expect to post per day?"
```text
{{ Realistic number }} distinct creators per day at launch, growing to
{{ number }} within {{ timeframe }}.
```
> Answer this honestly. TikTok sets your post-audit creator cap from this number, and asking for a large cap without matching traffic invites scrutiny. You can request an increase later through TikTok's developer support form once the app is audited and in production.
### "Describe the user experience for posting content"
```text
1. The creator selects which connected TikTok account to post to. We call
creator_info/query at that moment and display the returned nickname and
avatar so it is unambiguous which account will receive the content. If the
creator cannot currently post, we block posting and prompt them to retry
later.
2. The creator selects a video or up to 35 photos from their own library. We
render a preview of exactly what will be posted in a portrait frame matching
how it will appear on TikTok, with the caption over the video and the
destination handle shown. Video previews are playable, so the creator can
watch the file back before committing. No watermark, logo or overlay is added
to their content. We validate the video against max_video_post_duration_sec.
3. The creator writes their own caption. Any text we prefill remains fully
editable.
4. The creator selects who can view the post from a dropdown that is populated
only from privacy_level_options and has no default value.
5. The creator opts in to Comment / Duet / Stitch. All three are off by default
and are greyed out when creator_info reports that the creator has disabled
them. For photo posts only Comment is shown.
6. The creator optionally turns on content disclosure and picks "Your brand",
"Branded content" or both. The toggle is off by default; if it is on and
neither option is selected, the Post button stays disabled with the hover
text "You need to indicate if your content promotes yourself, a third party,
or both." Branded content cannot be combined with a private post - "Only me"
is disabled with the hover text "Branded content visibility cannot be set to
private."
7. The creator explicitly agrees to TikTok's Music Usage Confirmation (and
Branded Content Policy when branded content is selected) via a linked
consent checkbox, and presses Post.
8. We tell the creator that publishing may take a few minutes and poll
publish/status/fetch until we reach PUBLISH_COMPLETE, SEND_TO_USER_INBOX or
FAILED, surfacing the real outcome including the failure reason.
```
### "Per-scope justification"
Answer each enabled scope separately. Reviewers reject applications where a scope is enabled but never demonstrated.
| Scope | Justification |
| ------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `user.info.basic` | Display the connected creator's avatar and display name so the user can confirm which account they are posting to. |
| `user.info.profile` | Show profile link and verification status on the connected accounts screen. |
| `video.upload` | Send content to the creator's TikTok inbox as a draft when they choose to finish the post inside the TikTok app. |
| `video.publish` | Publish directly to the creator's profile after they complete our post form and give explicit consent. |
| `user.info.stats` | Display follower and engagement counts on the creator's own analytics dashboard. |
| `video.list` | Retrieve the creator's own public posts so we can show per-post metrics for content they published through us. |
## Video demo template
The demo video is where most applications fail. Reviewers use it to verify every point of the Direct Post guidelines, so **every required UI element must be visible on screen and readable**. Record at 1080p or better, use a real TikTok account, and do not cut away mid-flow.
**Length:** 3-5 minutes. **Format:** unlisted link, one continuous screen recording per flow, no fast-forwarding through the post form.
### Shot list
| # | Duration | What must be on screen | Guideline covered |
| -- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
| 1 | 0:00-0:20 | Your product's home or dashboard, showing your brand. State the product name and what it does. | Intended use |
| 2 | 0:20-0:50 | The full OAuth flow: click "Connect TikTok", the TikTok consent screen with **every requested scope visible**, then the redirect back showing the account connected. | Scope demonstration |
| 3 | 0:50-1:10 | The composer with the account selector open, then selected. **Zoom in on the creator nickname and avatar** rendered from `creator_info`. | Point 1 |
| 4 | 1:10-1:40 | Selecting a video from the user's own library, and the resulting preview. **Play the video in the preview for a few seconds** so the reviewer can see it is the real file, not a placeholder frame. Show the max duration hint sourced from `max_video_post_duration_sec`. Make it obvious no watermark or logo is added. | Points 1, 5 |
| 5 | 1:40-1:55 | Typing a caption by hand. If you prefill anything, show that it is editable. | Point 2a |
| 6 | 1:55-2:15 | Opening the privacy dropdown. **Pause with it open** so the reviewer can see there is no default selected and the options match `privacy_level_options`. Then select one. | Point 2b |
| 7 | 2:15-2:35 | The Comment / Duet / Stitch checkboxes, **all unchecked**. Toggle one on. If the test account has any disabled, hover it to show the greyed-out state and explanation. | Point 2c |
| 8 | 2:35-3:05 | The disclosure toggle **off**, then on. Check "Your brand" and show the "Promotional content" label. Check "Branded content" and show the "Paid partnership" label. Open the privacy dropdown again and **hover "Only me" to show the disabled state and its hover text**. Turn the toggle on with nothing selected and hover the disabled Post button to show its hover text. | Point 3 |
| 9 | 3:05-3:20 | The compliance declaration, close enough to read. Show it change to include the Branded Content Policy when branded content is selected, and that both links open TikTok's policy pages. | Point 4 |
| 10 | 3:20-3:40 | Pressing Post, then the "may take a few minutes to process" message and any status indicator. | Point 5 |
| 11 | 3:40-4:10 | The published post live on the TikTok profile or app, with the settings you chose reflected (visibility, comments off/on, the disclosure label). | Point 5 |
| 12 | 4:10-4:40 | Optional but recommended: repeat shots 3-11 for a photo carousel, showing that Duet and Stitch are absent and only Comment appears. | Photo post rules |
## UI requirements reviewers check
These are the specific things a reviewer clicks on. Each maps to a numbered point in TikTok's [Direct Post developer guidelines](https://developers.tiktok.com/doc/content-sharing-guidelines#direct_post_api_-_developer_guidelines).
### Point 1 - Creator info
* Call Outstand's [`GET /v1/tiktok/accounts/{id}/creator-info`](/docs/platform-apis/tiktok/get-tiktok-creator-info) from your server (requires `video.publish`; supports managed keys and BYOK) **when the post page renders and again whenever the selected account changes**. A cached response from an earlier session is a fail.
* Keep submission disabled while loading or after any failure; offer a retry that fetches fresh information. See the [server-side integration example](/docs/configurations/tiktok#fetch-current-creator-information).
* Display `creator_nickname` (and ideally `creator_avatar_url`) on the post page.
* If the creator cannot currently post - `privacy_level_options` is empty or `max_video_post_duration_sec` is `0` - **stop the attempt** and prompt them to retry later.
* Validate the selected video's duration against `max_video_post_duration_sec` before allowing submission.
### Point 2 - Post metadata
**2a - Title.** The creator must be able to enter their own title. Prefilled text and hashtags are allowed only if they remain editable.
**2b - Privacy.** A dropdown with **no default value**, populated exclusively from `privacy_level_options`. Never hardcode the four enum values - a creator whose account is private will not have `PUBLIC_TO_EVERYONE` in their options, and sending it fails with `privacy_level_option_mismatch`. Show human-readable labels:
| API value | Label |
| ----------------------- | --------- |
| `PUBLIC_TO_EVERYONE` | Public |
| `MUTUAL_FOLLOW_FRIENDS` | Friends |
| `FOLLOWER_OF_CREATOR` | Followers |
| `SELF_ONLY` | Only me |
**2c - Interaction toggles.** Allow Comment, Allow Duet and Allow Stitch, **all unchecked by default**. Grey out and disable each one when `comment_disabled`, `duet_disabled` or `stitch_disabled` is true in `creator_info`. For photo posts show only Allow Comment - Duet and Stitch do not apply.
> The UI states "allow" but the API takes the inverse. Send `disable_comment: !allowComment`. Getting this backwards is a silent correctness bug that the audit will not catch but your users will.
### Point 3 - Commercial content disclosure
* A disclosure toggle, **off by default**.
* When on, two checkboxes: **Your brand** (`brand_organic_toggle`) and **Branded content** (`brand_content_toggle`). At least one must be selected.
* While the toggle is on and neither box is checked, **the Post button must be disabled**, with this hover text, verbatim:
> You need to indicate if your content promotes yourself, a third party, or both.
* Show the resulting label, verbatim:
| Selection | Label shown |
| -------------------- | ----------------------------------------------------------- |
| Your brand only | `Your photo/video will be labeled as 'Promotional content'` |
| Branded content only | `Your photo/video will be labeled as 'Paid partnership'` |
| Both | `Your photo/video will be labeled as 'Paid partnership'` |
* Branded content cannot be private. Either disable the "Only me" option with this hover text, verbatim:
> Branded content visibility cannot be set to private.
or auto-switch visibility to public and tell the user you did. Do not silently allow the combination - TikTok will reject the post.
### Point 4 - Compliance declaration
Display one of these strings above the Post button, with the named policies as working links:
| Condition | String |
| ----------------------------------- | --------------------------------------------------------------------------------------- |
| Anything other than branded content | `By posting, you agree to TikTok's Music Usage Confirmation` |
| Branded content selected | `By posting, you agree to TikTok's Branded Content Policy and Music Usage Confirmation` |
Link targets:
* Music Usage Confirmation - `https://www.tiktok.com/legal/page/global/music-usage-confirmation/en`
* Branded Content Policy - `https://www.tiktok.com/legal/page/global/bc-policy/en`
### Point 5 - Content and consent
* **Show a preview** of the content before posting. The guideline itself is one sentence - "API Clients should display a preview of the to-be-posted content" - but see below for what a preview that actually passes looks like.
* **Never add a watermark, logo or overlay** to the creator's content. This is an instant fail.
* **Explicit consent:** content may only be sent to TikTok after the user presses a clear post/share action. Auto-posting on upload, or on a schedule the user did not configure per post, is not acceptable.
* **Warn about processing time.** After submitting, tell the user it may take a few minutes for the content to appear.
* **Poll `/v2/post/publish/status/fetch/`** (or handle webhooks) and report the real outcome. Do not show success when TikTok has not confirmed it.
#### What the preview should actually look like
* **Make the video playable in the preview.** A `