Unified.to
All articles

How to Publish to Any Social Platform from Inside Your Product with Unified.to


September 8, 2026

Your product can publish to whichever social accounts a customer runs, through one connection and one method call.

sdk.social.createSocialPost takes the same object whether the customer is posting to Bluesky, LinkedIn, Instagram, or Google Business Profile, and returns the post with the id you use to confirm it published. Every write is executed directly against the customer's platform, so the post exists there when the request returns.

Why publishing is the hard part of a social integration

Any product whose customers manage a public presence eventually has to let them post from inside it. A recruiting product that publishes job openings, a listings product that pushes updates to a business profile, a support product that replies to a comment. The customer already has the accounts. Posting to them is the integration.

The difficulty is that each platform models a post in its own API, with its own media handling, its own required fields, and its own content rules. Some accept a body and nothing else. Some reject a post with no image attached. Some carry a headline, a button, or a scheduled publish time, and most do not. Building against each one means a separate integration, separate auth, and separate handling for pagination, rate limits, and errors, before you have written any of your own product.

Writing raises the stakes over reading. A bad read shows one user stale data. A bad write publishes to the customer's audience, under the customer's name, where everyone can see it. The Unified Social Media API gives you one Post object to write against, so you build the composer once and the variation moves into data you can check.

What you are building

An embedded publishing module: the customer connects their accounts, picks which to post to, composes once, and publishes without leaving your product.

  1. Authorize the customer's platforms and store one connection ID each
  2. List their profiles to render an account picker
  3. Build a composer around the fields the target platform accepts
  4. Create the post, then read its status back to confirm what published
  5. Update, remove, or reply where the platform supports it

Connect once and list the customer's accounts

Initialize the SDK with your API key. This is the same for every category.

import { UnifiedTo } from '@unified-api/typescript-sdk';
import * as errors from '@unified-api/typescript-sdk/sdk/models/errors';

const sdk = new UnifiedTo({
  security: {
    jwt: process.env.UNIFIED_API_KEY,
  },
});

If your customers require their data to stay in a region, pass serverIdx when initializing: 0 for the North American server, 1 for European, 2 for Australian.

Each end-customer authorizes their own accounts through Unified's authorization component, which returns a connection ID. Store one per customer. That ID is the only thing that changes between a customer posting to Mastodon and a customer posting to Pinterest.

A post belongs to a profile, so before anyone can compose you need to know which accounts the connection can post from:

const profiles = await sdk.social.listSocialProfiles({
  connectionId,
  fields: 'id,name,username,avatar_url',
  limit: 50,
});

id, name, and username are your floor. Every integration in the category returns all three, which is enough to render a working account picker on day one. The fields parameter is worth using here rather than later: an account picker needs four values, and without it every profile arrives carrying its description, postal address, category, and audience metrics as well.

Hold the id of whichever profile the customer selects. That becomes profile_id on the post.

What each field does, and what it costs you to leave it out

The Post object carries more than a text box. Six fields decide what your composer looks like, and most publishing modules built against a single platform never expose them.

body is the text of the post, and it is the field to build your minimum interface around. Everything else is an enhancement.

media is an array, and each asset in it carries more than a link. url is required and is how you supply the asset, so your composer uploads to your own storage first and passes Unified the URL rather than the bytes. Alongside it, alt is the accessibility caption and position is the ordering within the post, where 1 is first. Both are yours to set. A composer that collects alt text and lets the customer drag images into order is doing something most embedded publishing modules do not, and it costs you two fields. type distinguishes PHOTO, VIDEO, and GIF, and height and width are available where the platform reports them.

title is an optional headline. It exists on the object and lands on a minority of platforms, so treat it as a field your composer reveals rather than one it always shows.

call_to_action is an object with a type and a url, where the type is one of BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, or CALL. This is the field that turns a post into something a customer can act on, and it is the clearest case of a control that should appear only when the selected account supports it. Offering a SHOP button on a platform that has no buttons is a support ticket.

type is the kind of post: POST, REPLY, REPOST, QUOTE, and STORY, plus the EVENT, OFFER, and ALERT formats that business listings use. If your product publishes to listings as well as feeds, this is the field that separates a weekly update from a promotional offer.

parent_post_id is the post this one replies to or quotes. Threading and replies are the same create call with this field set, not a separate endpoint.

language is the content's language code, which matters if your customers publish to more than one market from the same account.

Building a composer for platforms that accept different fields

Each platform's own API determines what Unified can write, so a field on the object may not be writable on every integration. This is a first-class part of building on any category, and three habits handle it.

Check support before you scope. Writable fields, required fields, and available methods are documented per integration on the Supported Integrations page. Method support varies too, not only fields: an integration may support create without supporting update or remove. If your feature depends on a call-to-action button or on editing a published post, confirm which platforms allow it before you promise it.

Some fields are required, not optional. Instagram and Pinterest will not publish a post with no media attached, because neither platform accepts a text-only post. That is the platform's rule and a unified layer inherits it. The capability matrix marks required fields per integration, and your composer should enforce them client-side rather than surfacing a failed write to the customer after they hit publish.

Treat 501 as information, not failure. Where a platform's API does not support an object or a method, the request returns 501 Not Implemented rather than an error you should retry. A 501 on create means this account cannot be posted to through the API. Show that differently from a network failure.

try {
  const post = await sdk.social.createSocialPost({ connectionId, socialPost });
  return renderPublished(post);
} catch (error) {
  if (error instanceof errors.UnifiedToError && error.statusCode === 501) {
    return renderUnsupported();
  }
  throw error;
}

UnifiedToError is the base class for every HTTP error response and carries statusCode alongside the message, headers, and body, so the same pattern handles authorization failures and rate limiting next to the unsupported case.

Build the composer around body, enhance upward. A module where text and a single image work everywhere, and title, buttons, and carousels appear when the account supports them, works across the whole category. A module that assumes every field is available works on one platform.

Create the post, then confirm what published

The create call takes the profile, the content, and whatever the platform accepts:

const post = await sdk.social.createSocialPost({
  connectionId,
  socialPost: {
    profile_id: selectedProfileId,
    body: 'We are hiring two senior engineers in Toronto.',
    type: 'POST',
    media: [
      {
        url: 'https://cdn.yourapp.com/assets/hiring-2026.png',
        alt: 'Two open engineering roles, Toronto office',
        type: 'PHOTO',
        position: 1,
      },
    ],
  },
});

The write is executed against the customer's platform directly, so the post their team sees in the platform's own app reflects it when your request returns.

Publishing is not always instant on the platform's side, though, and this is the part most modules get wrong. Store the returned id and read the post back:

const published = await sdk.social.getSocialPost({
  connectionId,
  id: post.id,
  fields: 'status,url',
});

status is where the real answer lives. PUBLISHED means it went out. PROCESSING means the platform is still encoding or reviewing it, which is common with video. REJECTED means the platform declined it, usually on content rules, and it is the state a customer most needs to see, because to them the post has simply vanished. DRAFT and SCHEDULED describe posts that exist but have not gone out.

url is the public permalink. Show it as soon as status is PUBLISHED, because the first thing a customer does after posting from inside your product is check that it looks right on the platform.

Scheduling, and where it lives

The Post object expresses scheduling: scheduled_at carries the intended publish time and the status enum carries a SCHEDULED value. Whether a given integration accepts scheduled_at on a write depends on whether its platform supports scheduling in its own API, and that is documented per integration in the capability matrix.

Where it is not supported, the queue stays in your application. You hold the post, and you call create when it is due. That is worth designing for from the start rather than discovering later, because a content calendar is the first thing customers ask a publishing module for, and building it on your own scheduler means it behaves the same way on every account.

Update, remove, and reply

Editing a published post uses the same object with the post's id:

const updated = await sdk.social.updateSocialPost({
  connectionId,
  id: post.id,
  socialPost: {
    body: 'We are hiring three senior engineers in Toronto.',
  },
});

FLAG — confirm before publishing The endpoint is PUT /social/{connection_id}/post/{id} and takes the full SocialPost model. If PUT replaces rather than merges, the partial payload above would clear media and call_to_action on the post. Needs Alexey to confirm merge behaviour, and the example changed to send the full object if it does not merge.

Removing a post deletes it on the customer's platform:

await sdk.social.removeSocialPost({ connectionId, id: post.id });

Note that the status enum also carries DELETED. Those are two different events, and a publishing module should tell them apart. Calling remove is your product deleting the post. Reading DELETED back is someone having deleted it in the platform's own app, outside your product. Since the category has no deleted webhook event, that second case surfaces only when you read.

Replies and quotes are a create call with parent_post_id set to the post being replied to, and type set to REPLY or QUOTE.

Which of these methods a given integration supports is in the capability matrix. An account you can post to is not necessarily an account you can edit or delete on, so gate the edit and delete controls in your interface on the same check you use for fields.

Reading a customer's existing posts

A publishing module usually also shows what the account has already posted. listSocialPosts takes paging and filtering:

const posts = await sdk.social.listSocialPosts({
  connectionId,
  profile_id: selectedProfileId,
  sort: 'created_at',
  order: 'desc',
  limit: 25,
});

offset is zero-based, and you know you have reached the end when a response returns fewer records than the limit you asked for. updated_gte returns only records modified since a timestamp you supply, where the integration supports it, which is what makes an incremental read possible without maintaining a cursor. Not every integration supports every list parameter, which is recorded on the Supported Integrations page.

Each post carries a metrics object with like, comment, share, quote, view, and bookmark counts, so a module can show how a post performed next to the post itself, without a second request.

→ Start your 30-day free trial

→ Book a demo

Frequently asked questions

What happens on a platform that only accepts a body?

Your module still works. Build so body and a single media asset carry the interface, and treat title, call-to-action, carousels, and scheduling as enhancements that appear when the account supports them. The Supported Integrations page tells you which platforms accept which fields before you scope the feature.

Should I store the posts I publish, or read them back each time?

Both, for different jobs. Store the id that create returns, because that is how you read a specific post's status and permalink later with getSocialPost. Read at request time when you need the current state of a post you can already identify. List when you need to show a feed or search across posts, since query searches on the endpoint and anything beyond that has to happen in a database you control.

How do I publish the same content to several accounts at once?

One create call per profile. The composer collects the content once, then you issue a create for each selected profile_id, sending only the fields that account accepts. Because each call returns its own post with its own id and status, a partial failure is visible per account rather than collapsing the whole publish into one success or failure.

Can I reach fields the unified model does not cover?

Yes, on the same connection. fields=raw returns the original fields the platform sent, a raw object in the payload writes them, and the raw query parameter passes vendor-native parameters through to the source API for filtering the unified model does not express.

Author

Written by Mallory Greene

Mallory Greene is Head of Marketing at Unified.to. She writes about integration infrastructure, unified APIs, and MCP for technical teams. Based in Toronto.

All articles