cross-post API

Programmatically manage your social media accounts, create and schedule posts, upload media, and track analytics across all your connected platforms. New to cross-post? Visit the homepage to create an account and get your API key.

Base URL

URL
https://cross-post.app/api/v1

All requests and responses use JSON. Set the Content-Type: application/json header on all requests with a body.

All API endpoints require authentication. See the Authentication section below to get started.

Supported Platforms

cross-post supports the following platforms: Instagram, YouTube, TikTok, X (Twitter), Threads, Bluesky, and Pinterest.

Authentication #

Authenticate API requests using a Bearer token. Generate API keys from your cross-post dashboard under Settings > Developer API.

Include your API key in the Authorization header of every request:

curl
curl https://cross-post.app/api/v1/accounts \
  -H "Authorization: Bearer cp_live_xxxxx..."

API Key Format

API keys use the prefix cp_live_ followed by 40 hex characters. Keep your keys secret and never expose them in client-side code.

Scopes

Each API key can be assigned granular scopes that limit what it can access:

ScopeDescription
accounts:readList connected social accounts
accounts:writeConnect and disconnect social accounts
posts:readList and retrieve posts
posts:writeCreate and delete posts
media:writeUpload media files
analytics:readView analytics data
usage:readView subscription usage and limits
webhooks:readList registered webhooks
webhooks:writeCreate and delete webhooks
*Wildcard — grants all scopes
Requesting an endpoint without the required scope returns a 403 Forbidden error with code insufficient_scope.

Rate Limiting #

API requests are rate-limited to 60 requests per minute per API key by default. This limit is configurable per key. Rate limit information is included in every response via headers:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only present on 429 responses)

When you exceed the limit, the API returns 429 Too Many Requests. Use the Retry-After header to determine how long to wait before retrying.

Response — 429
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Try again in 42 seconds",
    "status": 429
  }
}

Error Handling #

The API uses standard HTTP status codes and returns errors in a consistent format:

JSON
{
  "error": {
    "code": "validation_error",
    "message": "The caption field is required.",
    "status": 400
  }
}

Error Codes

CodeStatusDescription
auth_required401Missing or invalid Authorization header
invalid_api_key401API key is invalid or has been revoked
insufficient_scope403API key lacks the required scope for this endpoint
validation_error400Request body or parameters failed validation
not_found404The requested resource does not exist
method_not_allowed405HTTP method not supported for this endpoint
rate_limit_exceeded429Too many requests — slow down
limit_exceeded403Account usage limit reached for your plan
late_api_error500An error occurred communicating with the upstream publishing service
internal_error500An unexpected error occurred on our end

Pagination #

List endpoints return paginated results. Control pagination with query parameters:

ParameterDefaultMaxDescription
page1Page number
per_page20100Items per page

Paginated responses include a pagination object:

JSON
{
  "data": [...],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 47,
    "has_more": true
  }
}

List Accounts #

GET /api/v1/accounts accounts:read

Returns all social accounts connected to your cross-post account.

Response

JSON
{
  "data": [
    {
      "id": 12,
      "platform": "instagram",
      "username": "mycreatoraccount",
      "display_name": "My Creator Account",
      "avatar_url": "https://...",
      "connected_at": "2026-01-15 10:30:00",
      "is_active": true
    }
  ]
}

Example

curl
curl https://cross-post.app/api/v1/accounts \
  -H "Authorization: Bearer cp_live_xxxxx..."

Connect Account #

POST /api/v1/accounts/connect accounts:write

Generates an OAuth connect URL for the specified platform. Redirect the user to this URL to authorize the connection.

Request Body

FieldTypeRequiredDescription
platformstringrequiredOne of: instagram, youtube, tiktok, twitter, threads, bluesky, pinterest

Response

JSON
{
  "data": {
    "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
    "platform": "instagram"
  }
}

Example

curl
curl -X POST https://cross-post.app/api/v1/accounts/connect \
  -H "Authorization: Bearer cp_live_xxxxx..." \
  -H "Content-Type: application/json" \
  -d '{"platform": "instagram"}'

Disconnect Account #

DELETE /api/v1/accounts/{id} accounts:write

Disconnects a social account. Any scheduled posts targeting this account will fail.

Path Parameters

ParameterDescription
idThe account ID (integer, e.g., 12)

Response

JSON
{
  "data": {
    "success": true
  }
}

Example

curl
curl -X DELETE https://cross-post.app/api/v1/accounts/12 \
  -H "Authorization: Bearer cp_live_xxxxx..."

List Posts #

GET /api/v1/posts posts:read

Returns a paginated list of your posts. Supports filtering by status and platform.

Query Parameters

ParameterTypeDefaultDescription
statusstringFilter by status: draft, scheduled, publishing, published, partial, failed
platformstringFilter by target platform (e.g., instagram)
pageinteger1Page number
per_pageinteger20Results per page (max 100)

Response

JSON
{
  "data": [
    {
      "id": 42,
      "caption": "Check out our new feature!",
      "status": "published",
      "publish_mode": "now",
      "scheduled_at": null,
      "published_at": "2026-03-15 14:22:00",
      "created_at": "2026-03-15 14:20:00",
      "media": [
        {
          "url": "https://cdn.cross-post.app/media/abc123.jpg",
          "filename": "abc123.jpg",
          "mime_type": "image/jpeg",
          "size": 284720,
          "sort_order": 0
        }
      ],
      "destinations": [
        {
          "account_id": 12,
          "platform": "instagram",
          "status": "published",
          "published_at": "2026-03-15 14:22:00",
          "error_message": null
        }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 47,
    "has_more": true
  }
}

Get Post #

GET /api/v1/posts/{id} posts:read

Retrieves a single post by ID, including its media and per-destination status.

Response

Returns the same post object shape as the list endpoint, wrapped in a data key (not an array).

Example

curl
curl https://cross-post.app/api/v1/posts/42 \
  -H "Authorization: Bearer cp_live_xxxxx..."

Create Post #

POST /api/v1/posts posts:write

Creates a new post and publishes it to the specified social accounts. Upload media first using the media upload endpoint, then reference the returned URLs here.

Request Body

FieldTypeRequiredDescription
captionstringoptionalPost caption / text content
media_urlsarrayoptionalArray of media URLs (strings) or objects with url and type fields
destination_account_idsinteger[]requiredAccount IDs to publish to (integers)
publish_modestringoptionalnow, schedule, or draft. Defaults to schedule.
scheduled_atstringif scheduleDatetime string (e.g., 2026-03-20T15:00:00). Must be in the future.
timezonestringoptionalIANA timezone (e.g., America/New_York). Defaults to UTC.

Idempotency

This endpoint supports idempotency. Pass an X-Idempotency-Key header with a unique string to safely retry requests without creating duplicate posts. Cached responses expire after 24 hours.

Response

JSON
{
  "data": {
    "id": 57,
    "caption": "Check out our new feature!",
    "status": "scheduled",
    "publish_mode": "schedule",
    "scheduled_at": "2026-03-20T15:00:00",
    "published_at": null,
    "created_at": "2026-03-17 10:00:00",
    "media": [
      {
        "url": "https://cdn.cross-post.app/media/abc123.jpg",
        "filename": "abc123.jpg",
        "mime_type": null,
        "size": null,
        "sort_order": 0
      }
    ],
    "destinations": [
      {
        "account_id": 12,
        "platform": "instagram",
        "status": "pending",
        "published_at": null,
        "error_message": null
      },
      {
        "account_id": 15,
        "platform": "twitter",
        "status": "pending",
        "published_at": null,
        "error_message": null
      }
    ]
  }
}

Example

curl
curl -X POST https://cross-post.app/api/v1/posts \
  -H "Authorization: Bearer cp_live_xxxxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Check out our new feature!",
    "media_urls": ["https://cdn.cross-post.app/media/abc123.jpg"],
    "destination_account_ids": [12, 15],
    "publish_mode": "schedule",
    "scheduled_at": "2026-03-20T15:00:00",
    "timezone": "America/New_York"
  }'

Delete Post #

DELETE /api/v1/posts/{id} posts:write

Deletes a post. Only posts with status draft, scheduled, or failed can be deleted. Published or publishing posts cannot be deleted.

Response

JSON
{
  "data": {
    "success": true
  }
}

Example

curl
curl -X DELETE https://cross-post.app/api/v1/posts/42 \
  -H "Authorization: Bearer cp_live_xxxxx..."

Upload Media #

POST /api/v1/media/upload media:write

Returns a presigned upload URL. Upload your file with a PUT request to the upload_url, then use the public_url when creating posts.

Request Body

FieldTypeRequiredDescription
filenamestringrequiredOriginal filename with extension
content_typestringrequiredMIME type (e.g., image/jpeg, video/mp4)

Response

JSON
{
  "data": {
    "upload_url": "https://storage.cross-post.app/presigned/...",
    "public_url": "https://cdn.cross-post.app/media/abc123.jpg"
  }
}

Two-Step Upload Flow

Step 1: Call this endpoint to get the presigned URL.
Step 2: PUT the raw file to upload_url with the matching Content-Type header. Then reference public_url in your create post request.

Example

curl
# Step 1: Get presigned URL
curl -X POST https://cross-post.app/api/v1/media/upload \
  -H "Authorization: Bearer cp_live_xxxxx..." \
  -H "Content-Type: application/json" \
  -d '{"filename": "photo.jpg", "content_type": "image/jpeg"}'

# Step 2: Upload the file
curl -X PUT "https://storage.cross-post.app/presigned/..." \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

Get Analytics #

GET /api/v1/analytics analytics:read

Returns analytics for your account, including post counts, platform breakdown, and daily posting trends.

Query Parameters

ParameterTypeDefaultDescription
periodstring30dTime period: 7d, 30d, 90d, or all

Response

JSON
{
  "data": {
    "period": "30d",
    "total_posts": 142,
    "published_count": 128,
    "failed_count": 3,
    "scheduled_count": 11,
    "by_status": {
      "published": 128,
      "failed": 3,
      "scheduled": 11
    },
    "platforms": {
      "instagram": 45,
      "twitter": 38,
      "youtube": 22,
      "tiktok": 18,
      "threads": 12,
      "bluesky": 7
    },
    "success_rate": 97.7,
    "posts_over_time": [
      { "date": "2026-03-01", "count": 5 },
      { "date": "2026-03-02", "count": 3 }
    ]
  }
}

Get Usage #

GET /api/v1/usage usage:read

Returns your current usage counts and subscription tier limits. A limit value of -1 means unlimited.

Response

JSON
{
  "data": {
    "subscription_tier": "pro",
    "subscription_expires_at": "2026-04-15 00:00:00",
    "posts": {
      "used": 42,
      "limit": 500,
      "remaining": 458
    },
    "scheduled_posts": {
      "used": 8,
      "limit": 100,
      "remaining": 92
    },
    "social_accounts": {
      "used": 4,
      "limit": 15,
      "remaining": 11
    },
    "max_media_size_mb": 100
  }
}

List Webhooks #

GET /api/v1/webhooks webhooks:read

Returns all active webhook endpoints for your account. The secret is not included in list responses.

Response

JSON
{
  "data": [
    {
      "id": 3,
      "url": "https://example.com/webhooks/crosspost",
      "events": ["post_published", "post_failed"],
      "created_at": "2026-02-10 08:00:00"
    }
  ]
}

Create Webhook #

POST /api/v1/webhooks webhooks:write

Registers a new webhook endpoint. You will receive a secret in the response — store it securely for signature verification. Maximum 5 webhooks per user.

Request Body

FieldTypeRequiredDescription
urlstringrequiredHTTPS URL to receive webhook payloads
eventsstring[]requiredEvents to subscribe to (see events list)

Response

JSON
{
  "data": {
    "id": 3,
    "url": "https://example.com/webhooks/crosspost",
    "events": ["post_published", "post_failed"],
    "secret": "a1b2c3d4e5f6...",
    "created_at": "2026-03-17 10:00:00"
  }
}
The secret is only returned once at creation time. Store it securely — you will need it to verify webhook signatures.

Example

curl
curl -X POST https://cross-post.app/api/v1/webhooks \
  -H "Authorization: Bearer cp_live_xxxxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/crosspost",
    "events": ["post_published", "post_failed"]
  }'

Delete Webhook #

DELETE /api/v1/webhooks/{id} webhooks:write

Deletes a registered webhook. No further events will be delivered to this endpoint.

Response

JSON
{
  "data": {
    "success": true
  }
}

Example

curl
curl -X DELETE https://cross-post.app/api/v1/webhooks/3 \
  -H "Authorization: Bearer cp_live_xxxxx..."

Webhook Events & Signatures #

Available Events

EventDescription
post_createdA new post was created (any publish mode)
post_publishedA post was successfully published to all destinations
post_failedA post failed to publish on one or more destinations
account_connectedA new social account was connected
account_disconnectedA social account was disconnected

Delivery

Webhooks are delivered as POST requests to your registered URL with a JSON payload. Respond with a 2xx status code within 10 seconds to acknowledge receipt.

Retry Policy

Failed deliveries are retried up to 5 times with exponential backoff: 30 seconds, 2 minutes, 8 minutes, 32 minutes, 2 hours.

Payload Format

The webhook body is a JSON object with event, data, and timestamp fields. The data contents vary by event type.

JSON — post_created
{
  "event": "post_created",
  "data": {
    "post_id": 57,
    "status": "scheduled",
    "publish_mode": "schedule",
    "platform_count": 2
  },
  "timestamp": "2026-03-17T14:30:00+00:00"
}
JSON — account_disconnected
{
  "event": "account_disconnected",
  "data": {
    "account_id": 12,
    "platform": "instagram"
  },
  "timestamp": "2026-03-17T14:30:00+00:00"
}

Signature Verification

Every webhook delivery includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature of the raw request body: sha256={hex}
X-Webhook-EventThe event type (e.g., post_created)
User-Agentcross-post-webhooks/1.0
Content-Typeapplication/json

The signature is computed over the raw JSON body using your webhook secret as the HMAC key. Always verify signatures to ensure payloads are authentic.

The header format is: sha256={hex-encoded HMAC}

Node.js

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express middleware example
app.post('/webhooks/crosspost', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log('Received event:', event.event);
  res.sendStatus(200);
});

Python

Python
import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhooks/crosspost', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
        abort(401)

    event = request.get_json()
    print(f"Received event: {event['event']}")
    return '', 200

Code Examples #

Creating a Post

A complete example showing how to upload media and create a scheduled post across multiple platforms.

Bash
API_KEY="cp_live_xxxxx..."

# Step 1: Get presigned upload URL
UPLOAD_RESPONSE=$(curl -s -X POST https://cross-post.app/api/v1/media/upload \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "promo.jpg", "content_type": "image/jpeg"}')

UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r ".data.upload_url")
PUBLIC_URL=$(echo $UPLOAD_RESPONSE | jq -r ".data.public_url")

# Step 2: Upload file to presigned URL
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @promo.jpg

# Step 3: Create the post
curl -X POST https://cross-post.app/api/v1/posts \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"caption\": \"Exciting news! Check out what we have been working on.\",
    \"media_urls\": [\"$PUBLIC_URL\"],
    \"destination_account_ids\": [12, 15],
    \"publish_mode\": \"schedule\",
    \"scheduled_at\": \"2026-03-20T15:00:00\",
    \"timezone\": \"America/New_York\"
  }"
JavaScript
const API_KEY = 'cp_live_xxxxx...';
const BASE_URL = 'https://cross-post.app/api/v1';

async function createPost() {
  // Step 1: Get presigned upload URL
  const uploadRes = await fetch(`${BASE_URL}/media/upload`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      filename: 'promo.jpg',
      content_type: 'image/jpeg',
    }),
  });
  const { data: { upload_url, public_url } } = await uploadRes.json();

  // Step 2: Upload file to presigned URL
  const fileBuffer = await fs.promises.readFile('./promo.jpg');
  await fetch(upload_url, {
    method: 'PUT',
    headers: { 'Content-Type': 'image/jpeg' },
    body: fileBuffer,
  });

  // Step 3: Create the post
  const postRes = await fetch(`${BASE_URL}/posts`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      caption: 'Exciting news! Check out what we have been working on.',
      media_urls: [public_url],
      destination_account_ids: [12, 15],
      publish_mode: 'schedule',
      scheduled_at: '2026-03-20T15:00:00',
      timezone: 'America/New_York',
    }),
  });

  const post = await postRes.json();
  console.log('Post created:', post.data.id);
}

createPost();
Python
import requests

API_KEY = 'cp_live_xxxxx...'
BASE_URL = 'https://cross-post.app/api/v1'
headers = {'Authorization': f'Bearer {API_KEY}'}

# Step 1: Get presigned upload URL
upload_res = requests.post(f'{BASE_URL}/media/upload',
    headers=headers,
    json={'filename': 'promo.jpg', 'content_type': 'image/jpeg'}
)
upload_data = upload_res.json()['data']

# Step 2: Upload file to presigned URL
with open('promo.jpg', 'rb') as f:
    requests.put(upload_data['upload_url'],
        headers={'Content-Type': 'image/jpeg'},
        data=f.read()
    )

# Step 3: Create the post
post_res = requests.post(f'{BASE_URL}/posts',
    headers=headers,
    json={
        'caption': 'Exciting news! Check out what we have been working on.',
        'media_urls': [upload_data['public_url']],
        'destination_account_ids': [12, 15],
        'publish_mode': 'schedule',
        'scheduled_at': '2026-03-20T15:00:00',
        'timezone': 'America/New_York',
    }
)

post = post_res.json()
print(f"Post created: {post['data']['id']}")

Listing Accounts

Bash
curl https://cross-post.app/api/v1/accounts \
  -H "Authorization: Bearer cp_live_xxxxx..."
JavaScript
const res = await fetch('https://cross-post.app/api/v1/accounts', {
  headers: { 'Authorization': `Bearer ${API_KEY}` },
});
const { data: accounts } = await res.json();

accounts.forEach(account => {
  console.log(`${account.platform}: ${account.username} (${account.is_active ? 'active' : 'inactive'})`);
});
Python
import requests

res = requests.get('https://cross-post.app/api/v1/accounts',
    headers={'Authorization': f'Bearer {API_KEY}'}
)
accounts = res.json()['data']

for account in accounts:
    status = 'active' if account['is_active'] else 'inactive'
    print(f"{account['platform']}: {account['username']} ({status})")

Setting Up a Webhook

Bash
curl -X POST https://cross-post.app/api/v1/webhooks \
  -H "Authorization: Bearer cp_live_xxxxx..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/crosspost",
    "events": [
      "post_published",
      "post_failed",
      "account_connected"
    ]
  }'
JavaScript
const res = await fetch('https://cross-post.app/api/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://example.com/webhooks/crosspost',
    events: ['post_published', 'post_failed', 'account_connected'],
  }),
});

const { data: webhook } = await res.json();
console.log('Webhook ID:', webhook.id);
console.log('Secret (store this!):', webhook.secret);
Python
import requests

res = requests.post('https://cross-post.app/api/v1/webhooks',
    headers={'Authorization': f'Bearer {API_KEY}'},
    json={
        'url': 'https://example.com/webhooks/crosspost',
        'events': ['post_published', 'post_failed', 'account_connected'],
    }
)

webhook = res.json()['data']
print(f"Webhook ID: {webhook['id']}")
print(f"Secret (store this!): {webhook['secret']}")

Need help? Contact us at support@cross-post.app

Back to cross-post · Read the blog