Skip to content
Social SDK megaphone markSocial SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Upload-Post

Configure Upload-Post to publish text, photos, video, and LinkedIn documents, schedule posts, chain X and Bluesky replies, and verify webhooks.

Upload-Post is an optional managed backend. Your Upload-Post account holds profiles, and each profile connects one account per platform. The adapter publishes through Upload-Post’s REST API with one profile and one platform per request, so every destination keeps its own outcome.

import { readFile } from "node:fs/promises";
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { uploadPost } from "@opencoredev/social-sdk/cloud/upload-post";

const adapter = uploadPost({
  apiKey: process.env["UPLOAD_POST_API_KEY"]!,
  webhookSecret: process.env["UPLOAD_POST_WEBHOOK_SECRET"],
});
const social = createSocial({ backend: adapter });

// Get tenantId from your authenticated server session and enforce account grants.
const authorization = { tenantId: "tenant-from-session" };
const accounts = await social.accounts.list({ backend: "default", authorization });

const x = connectedAccountRef({ backend: "default", platform: "x", accountId: "my-brand" });
const linkedin = connectedAccountRef({
  backend: "default",
  platform: "linkedin",
  accountId: "my-brand",
});

The adapter sends the key as Authorization: Apikey <key>. Keep it on your server.

Accounts are profile and platform pairs

accounts.list reads /uploadposts/users and returns one account for each connected platform on each profile. The account ID is the Upload-Post profile username, the user value in every Upload-Post call. Platform and profile together identify the destination. A slot that needs reauthentication is reported as reconnect-required. Listing and publishing cover different sets. Listing returns slots on every network the SDK recognizes, such as Pinterest and Reddit. Publishing covers X, Threads, Bluesky, YouTube, TikTok, Instagram, LinkedIn, and Facebook; a target on any other listed network fails preparation with platform.unsupported before any request.

Slots on networks the SDK does not know are skipped. Pass unknownPlatforms: "include" to list them under Upload-Post’s own name, for reading only:

const adapter = uploadPost({
  apiKey: process.env["UPLOAD_POST_API_KEY"]!,
  unknownPlatforms: "include",
});

Publish

const result = await social.posts.publish(
  {
    targets: [{ account: x }, { account: linkedin }],
    content: { text: "Shipping today." },
    idempotencyKey: "launch-2026-10-24",
  },
  { authorization },
);

The adapter picks Upload-Post’s route from the content:

Content Route Platforms
Text only /upload_text X, Threads, Bluesky, LinkedIn, Facebook
Images (Instagram and Threads also accept mixed carousels) /upload_photos All except YouTube
One video /upload All eight
One PDF, PPT, PPTX, DOC, or DOCX /upload_document LinkedIn

Each request sends the SDK’s derived target key as Idempotency-Key, which Upload-Post uses to return the existing job on a retry instead of creating a duplicate.

A synchronous result with a native post_id becomes published. A success without a native post ID becomes unknown, because the SDK never reports accepted work as published. An async response (request_id) becomes accepted with a delivery reference.

Media

Upload-Post has no standalone upload endpoint that returns a reusable handle, so media.upload is unsupported. Pass media in the publish request as a public HTTPS URL, which Upload-Post downloads, or a Blob, which the adapter sends as a multipart file:

const deck = new Blob([await readFile("./assets/deck.pdf")]);

await social.posts.publish(
  {
    targets: [{ account: linkedin, options: { visibility: "public" } }],
    content: {
      text: "Our Q3 results",
      media: [
        {
          kind: "document",
          mimeType: "application/pdf",
          filename: "q3-results.pdf",
          source: { kind: "blob", blob: deck, fingerprint: "q3-results-v1" },
        },
      ],
    },
  },
  { authorization },
);

The document’s filename is shown as its title, and content.text is the commentary. Upload-Post documents no scheduling for documents, so a scheduled document fails preparation with upload_post.document_schedule instead of publishing immediately. Streams, media-ref sources, alt text, per-attachment captions, and thumbnails are rejected at preparation because Upload-Post documents no mapping for them.

Options

Platform Supported options Upload-Post fields
X replySettings reply_settings (everyone sends nothing)
YouTube title, visibility, madeForKids, categoryId title, privacyStatus, selfDeclaredMadeForKids, categoryId; content.text is the description
Instagram shareToFeed (video) share_to_feed
LinkedIn visibility visibility; video and documents default to PUBLIC, connections-only works for video and documents only
TikTok Every required choice, plus photoCoverIndex privacy_level, post_mode (draft), disable_*, brand toggles, AI disclosure

Any other option fails preparation with options.unmapped. Upload-Post strips URLs from X posts unless your account enables its link add-on. On Facebook, Upload-Post selects the Page automatically only when the profile has one connected Page.

Replies and thread chains

Upload-Post documents reply_to_id for X on every route and for Bluesky on text posts. A target with a platform-post replyTo on those destinations is published as a reply. That makes publishSequence with replyToPrevious work across several destinations, each building its own chain:

const bluesky = connectedAccountRef({
  backend: "default",
  platform: "bluesky",
  accountId: "my-brand",
});

await social.posts.publishSequence(
  {
    idempotencyKey: "launch-thread",
    replyToPrevious: true,
    items: [
      { targets: [{ account: x }, { account: bluesky }], content: { text: "1/ We shipped." } },
      {
        targets: [{ account: x }, { account: bluesky }],
        content: { text: "2/ Here's what changed." },
      },
    ],
  },
  { authorization },
);

Other platforms, Bluesky media replies, and comment references are rejected with reply.use_comments before anything is sent.

Schedule, status, and cancel

const scheduled = await social.posts.publish(
  {
    targets: [{ account: x }],
    content: { text: "See you Monday." },
    schedule: { at: "2026-11-02T09:00:00Z", timeZone: "Europe/Madrid" },
  },
  { authorization },
);

const outcome = scheduled.outcomes[0];

if (outcome?.state === "scheduled" && outcome.delivery) {
  const status = await social.posts.getDelivery(outcome.delivery, { authorization });
  await social.posts.cancelScheduled(outcome.job, { authorization });
}

schedule.at is sent as scheduled_date and timeZone as timezone. The response’s job_id becomes the scheduled job.

getDelivery reads /uploadposts/status. Pending jobs stay scheduled; queued or running uploads are processing; failed or skipped platforms are failed. Upload-Post’s status endpoint has no native post IDs, so for a completed upload the adapter looks up platform_post_id in the latest 100 entries of upload history. If it is not there, the outcome is unknown; check the Upload-Post dashboard before you publish again.

cancelScheduled first reads the job and refuses one that does not belong to exactly this profile and platform. It then deletes the job, which Upload-Post documents as deleting its assets too, so the result’s backendRecord is deleted.

Remove published posts

removeFromPlatform calls /uploadposts/posts/unpublish for Facebook, YouTube, X, LinkedIn, and Threads. Upload-Post cannot delete Instagram or TikTok posts. A response without success: true is reported as ambiguous_outcome.

Webhooks

Upload-Post signs each delivery with X-Upload-Post-Signature: sha256=<hex>, an HMAC-SHA256 of <X-Upload-Post-Timestamp>.<raw body> keyed with your account’s webhook_secret. Pass that secret as webhookSecret. adapter.webhooks.verify checks the raw bytes in constant time and rejects timestamps more than five minutes away from its clock.

const context = {
  backendInstance: "default",
  correlationId: "upload-post-webhook",
  retryBudget: { maxAttempts: 1, maxElapsedMs: 10_000 },
};

export async function POST(request: Request) {
  const body = new Uint8Array(await request.arrayBuffer());
  const input = { headers: request.headers, body };

  // Throws an `unauthorized` SocialError for a bad signature or stale timestamp.
  await adapter.webhooks.verify(input);
  const event = await adapter.webhooks.decode(input, context);
  // Store `event` in your inbox, then acknowledge.

  return new Response(null, { status: 204 });
}

upload_completed decodes to publication.updated with the request or job ID as backendRecordId; account connection changes decode to account.updated; comment_received and dm_received decode to comment.received and message.received. accountIds holds the profile username. The owner’s email address is removed from the decoded payload. Upload-Post sends no event ID, so the event’s identity is a digest of the body.

Limits

  • Comments, messages, post reads, and analytics are not implemented for this backend.
  • Upload-Post’s API reference is OpenAPI 1.0.0, checked 2026-10-24 at docs.upload-post.com. No live Upload-Post account was used to verify this adapter.

Last updated on October 26, 2026