UpTrended SocialUpTrended Social
Developers

Let your agent run the calendar.

An MCP server and a REST API over the same scheduler the dashboard uses. Point Claude, Cursor or your own agent at it and it can draft, validate and schedule posts to Instagram, Facebook, TikTok, X, Pinterest and YouTube.

https://uptrended.social/mcphttps://uptrended.social/api/v1npx -y @uptrended/mcp

Quickstart

Create a key, connect an MCP client, and get a first post on the calendar. About five minutes.

1

Create an API key

In the dashboard, go to Settings → API keys and create a key. Choose a scope preset while you are there — draft is the default and the one to start with: an agent using it can compose posts, but every post lands as a draft for you to approve.

utsk_live_a1b2c3d4e5f6.WKq7Lm3PxZ2vN8tRfY0sHcJ4bA6dE1gU9iO5nQwT2xM
The secret half is shown once, at creation. A key belongs to exactly one workspace — there is no organization_id parameter anywhere in the API, because the key already decides which workspace you are talking to.

API access is on paid plans only. See pricing — Hobby has no API access.

2

Connect Claude

Point Claude Code at the remote MCP server. Nothing to install.

terminal
claude mcp add --transport http uptrended-social \
  https://uptrended.social/mcp \
  --header "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"

For Claude Desktop, Cursor, VS Code and anything else that only speaks stdio, see MCP setup below.

3

Ask for a post

The agent should do three things in order: list your connected accounts, validate a draft payload against the platform rules, then create the post.

prompt
Draft a post announcing our Tuesday launch and put it on
the calendar for 9am Tuesday, Instagram and X. Validate it
before you create anything.

With a draft key it comes back as a draft, waiting for you in the dashboard. With a write key it is scheduled for real.

The workflow that works

Every posting task follows the same three steps, and skipping one is how agents produce payloads that fail:

list_accountsvalidate_postcreate_post
  • list_accounts — every target needs a real social_account_id. They cannot be guessed from a handle.
  • validate_post — a dry run. Writes nothing, consumes no quota, never touches a live account, and returns per-target errors. Iterate here until it is clean.
  • create_post — one call, all targets. Read the response rather than assuming it scheduled.

Connecting accounts is not in the API

Linking an Instagram, Facebook, TikTok, X, Pinterest or YouTube account is an interactive OAuth flow: the platform shows its own consent screen in a browser, and in several cases the person has to pick which Page, board or channel to grant. That cannot be driven headlessly, and shipping an API that pretends otherwise would just fail confusingly.

So: connect accounts once at uptrended.social, in the browser. After that they appear in list_accounts and in GET /api/v1/accounts, and everything else is programmable.

If an agent reports that it cannot find an account for a platform, that is the answer — the account has not been connected yet. There is no endpoint that would fix it.

MCP setup

UT Social is a remote Streamable HTTP MCP server. Use it directly if your client supports remote servers with custom headers; there is an npx bridge for the ones that don’t.

Remote (preferred)

Endpoint:  https://uptrended.social/mcp
Transport: Streamable HTTP
Header:    Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here

No install, no local process, and the tool list is always current. Claude Code:

terminal
claude mcp add --transport http uptrended-social \
  https://uptrended.social/mcp \
  --header "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"

stdio bridge (fallback)

For clients that can only spawn a local command — Claude Desktop, Cursor, VS Code — npx -y @uptrended/mcp proxies stdio to the same endpoint. It implements no tools of its own; it forwards JSON-RPC frames, so the server stays the only source of truth for what is available.

Claude Desktop · claude_desktop_config.json
{
  "mcpServers": {
    "uptrended-social": {
      "command": "npx",
      "args": ["-y", "@uptrended/mcp"],
      "env": {
        "UTSOCIAL_API_KEY": "utsk_live_xxxxxxxxxxxx.your-secret-here"
      }
    }
  }
}

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json. Windows: %APPDATA%\Claude\claude_desktop_config.json. Restart the app afterwards — it reads that file only at launch.

Cursor · .cursor/mcp.json
{
  "mcpServers": {
    "uptrended-social": {
      "command": "npx",
      "args": ["-y", "@uptrended/mcp"],
      "env": {
        "UTSOCIAL_API_KEY": "utsk_live_xxxxxxxxxxxx.your-secret-here"
      }
    }
  }
}
VS Code · .vscode/mcp.json
{
  "inputs": [
    {
      "id": "utsocial-key",
      "type": "promptString",
      "description": "UT Social API key",
      "password": true
    }
  ],
  "servers": {
    "uptrended-social": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@uptrended/mcp"],
      "env": { "UTSOCIAL_API_KEY": "${input:utsocial-key}" }
    }
  }
}
The VS Code config uses an input so the key is prompted for and kept in secret storage rather than committed to the repo. Do the same anywhere the config file is version controlled.

Bridge configuration

VariableRequiredDefault
UTSOCIAL_API_KEYyes
UTSOCIAL_BASE_URLnohttps://uptrended.social

The bridge validates the key format locally and writes every diagnostic to stderr, because stdout is the MCP wire. In Claude Desktop those land in ~/Library/Logs/Claude/mcp-server-uptrended-social.log.

Tools

Tools are filtered by the key’s scopes. A read-only key does not get refused when it calls create_post — it never sees the tool at all, which stops an agent from planning around a capability it does not have.

ToolDoesScope
list_accountsConnected accounts and their idsaccounts:read
list_mediaMedia already in the workspace librarymedia:read
ingest_mediaFetch a public URL server-side (100 MB cap)media:write
validate_postDry run. No writes, no quota, no live account touchedposts:read
create_postCreate a post across every target at onceposts:draft (+ posts:write to schedule)
get_postOne post, with per-target statusposts:read
list_postsKeyset-paginated post listposts:read
cancel_postStop a scheduled post, keep the recordposts:write
reschedule_postMove a scheduled postposts:write
delete_postRemove a post entirelyposts:write
get_usageQuota used and remainingusage:read

Why validate_post matters more than it sounds

Platform rules are inconsistent enough that a payload which looks obviously fine is often not. validate_post takes the exact object you would pass to create_post and reports what each target would reject — for free, and without publishing anything. Two or three validate round-trips before a create is normal.

Per-platform rules it catches

  • X — 280 weighted characters. Every URL counts as 23 no matter its real length, and CJK characters and most emoji count double. A 250-character draft plus two links does not fit.
  • YouTube — descriptions cap at 5,000 UTF-8 bytes, not characters. Emoji are four bytes each, so an emoji-heavy description can fail well under 5,000 characters. Titles cap at 100 characters.
  • Instagram — captions cap at 2,200 characters, and post_and_story forks into two independent targets that succeed or fail separately. Report per-target results, not one verdict.
  • TikTok — needs an explicit music-usage agreement flag, which is a legal acknowledgement and has no sensible default. Apps that have not completed TikTok’s audit are restricted to SELF_ONLY visibility; that is TikTok’s rule and cannot be worked around.
  • Pinterest — a pin needs a board_id. Valid boards come back with the account in list_accounts.
  • Facebook — Pages only. Personal profiles are not publishable through the platform API.

Quota is per post, not per target

One post fanned out to six platforms is one unit of quota. Six single-platform posts are six. Put every target in a single create_post call unless the content itself genuinely differs per platform — and even then, per-target overrides are usually the better answer than separate posts.

REST API

Everything the MCP server does is also plain HTTP, with identical shapes.

terminal
curl https://uptrended.social/api/v1/accounts \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here"

Conventions

  • Base URL https://uptrended.social/api/v1. Every request needs Authorization: Bearer utsk_live_….
  • Collections return { data: [...], next_cursor, has_more } and paginate by keyset. Follow next_cursor while has_more is true; there are no page numbers.
  • Errors return { error, code, request_id }. Quote the request_id when reporting a problem.
  • organization_id is never accepted in a request body. The key determines the workspace.
  • Mutating calls accept an Idempotency-Key header.

Endpoints

EndpointDescriptionScope
GET/meKey identity, workspace, plan, granted scopes
GET/usageQuota used and remaining for the periodusage:read
GET/accountsConnected social accounts and their idsaccounts:read
GET/mediaMedia librarymedia:read
POST/media/ingestFetch a public URL server-side. 100 MB capmedia:write
POST/media/upload-urlSigned URL for a direct upload. Larger filesmedia:write
POST/media/registerRegister an upload once the bytes have landedmedia:write
DELETE/media/:idDelete a media itemmedia:write
GET/postsList posts. Keyset paginatedposts:read
POST/posts/validateDry run. Writes nothing, no quotaposts:read
POST/postsCreate a post across all targetsposts:draft (+ posts:write to schedule)
GET/posts/:idOne post with per-target statusposts:read
POST/posts/:id/cancelStop a scheduled post, keep the recordposts:write
POST/posts/:id/rescheduleMove it. Cheaper than delete-and-recreateposts:write
POST/posts/:id/retryRetry failed targetsposts:write
DELETE/posts/:idDelete a postposts:write

Validate, then create

terminal
curl https://uptrended.social/api/v1/posts/validate \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Launching Tuesday. Here is what changed.",
    "scheduled_at": "2026-08-04T09:00:00-05:00",
    "targets": [
      { "social_account_id": "acc_2f9…", "kind": "post" },
      { "social_account_id": "acc_7c1…", "kind": "post" }
    ]
  }'

A clean validate returns no target errors. Then send the same body to POST /api/v1/posts with an Idempotency-Key.

With a draft-scoped key, that create saves a draft and does not schedule it. The response says so. Read the returned status rather than treating 200 as “scheduled”.

Media

Two ways to get bytes into a post.

  • Ingest a URL POST /media/ingest (or the ingest_media tool) fetches a publicly reachable URL server-side. 100 MB cap. This covers most cases.
  • Direct upload POST /media/upload-url returns a signed URL, you PUT the bytes to it, then POST /media/register to add it to the library. Use this for larger files or local bytes.

Check list_media before re-ingesting an asset you already have. Media carries its own per-platform constraints — aspect ratio, duration, codec — and validate_post checks those too, which is one more reason to validate before creating.

Scopes

A key carries one of three presets. Pick the least it needs — an agent cannot exceed its key, so the scope is the actual safety boundary, not the prompt.

read

Look, don’t touch

Accounts, posts, media and usage, read-only. Mutating tools are not even listed.

draft

Default · recommended

Everything read can do, plus create_post — but posts are saved as drafts for a human to approve, never scheduled.

write

Publishes for real

Schedules and publishes to live accounts, and can cancel, reschedule and delete. Give this out deliberately.

Presets expand into the granular scopes named in the tables above — accounts:read, media:write, posts:draft, posts:write, usage:read. GET /api/v1/me returns exactly what a key was granted.

Draft behaviour is the part agents get wrong. A 200 from create_post on a draft key means “saved as a draft”, not “scheduled”. The response distinguishes them; anything reporting back to a human should read it.

Plans & rate limits

PlanAPI accessRequests / min / keyKeys
Hobby — freeNone0
Starter — $19Yes602
Growth — $49Yes1205
Scale — $99Yes30020

Rate limits are per key per minute, so splitting traffic across two keys on the same workspace genuinely doubles headroom — up to the key cap. Exceeding a cap returns 402 PLAN_REQUIRED at key-creation time, not at request time.

A rate-limited response carries Retry-After plus RateLimit-* headers. Honour Retry-After rather than retrying on a fixed timer.

Posting quota is separate from the rate limit, and it counts posts, not targets or requests. One post to six platforms is one unit. Check GET /api/v1/usage before a large batch.

Idempotency

Every mutating call accepts an Idempotency-Key header. Use it, and reuse it on retry.

terminal
curl https://uptrended.social/api/v1/posts \
  -H "Authorization: Bearer utsk_live_xxxxxxxxxxxx.your-secret-here" \
  -H "Idempotency-Key: 6f1c9e2a-4b77-4a10-9d3e-8f2b5c0a71de" \
  -H "Content-Type: application/json" \
  -d @post.json
  • Generate one fresh UUID per logical post.
  • On a retry — timeout, 5xx, dropped connection — send the same key again. The server returns the original result instead of creating a second post.
  • Generating a new key on retry is how you double-post to a real audience. It is the most damaging mistake available here and the easiest to avoid.
  • If a create failed ambiguously and no key was sent, do not blindly retry — call GET /api/v1/posts and check whether it landed.

Errors

Every error is { error, code, request_id }. 401, 402 and 403 are terminal — retrying will not resolve them.

StatusCodeMeaningDo
401UNAUTHORIZEDKey invalid, revoked or deletedStop. Issue a new key in Settings → API keys.
402PLAN_REQUIREDPlan does not include API access, or a cap was hitStop. Upgrade or free up a key slot.
403INSUFFICIENT_SCOPEThe key’s scopes do not cover this callStop. Do not route around it with another endpoint.
404NOT_FOUNDUnknown id, or an id belonging to another workspaceRe-list. A stale id from earlier is the usual cause.
422VALIDATION_FAILEDPayload rejected, with per-target detailFix it and run validate again.
429RATE_LIMITEDPer-key per-minute limit exceededBack off for Retry-After seconds.

Cross-workspace ids return 404 rather than 403 on purpose — a key should not be able to probe for the existence of resources it cannot reach.

Machine-readable summary

Point an agent at /docs/llms.txt for a plain-text version of this page.

Open llms.txt