> ## Documentation Index
> Fetch the complete documentation index at: https://docs.so-me.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Posts and Media

> Publish immediately, schedule posts, and upload media for embedded customers

Posts belong to an end user and must target one of that end user's social
connections. The `connectionId` platform must match `socialMedia`.

## Create a text post

Omit `scheduledAt` to queue the post immediately:

```ts theme={null}
const post = await soMe.posts.create(
  endUser.id,
  {
    connectionId: connection.id,
    socialMedia: "THREADS",
    postType: "TEXT",
    text: "Published through our app",
  },
  { idempotencyKey: "customer_123:post_456" },
);
```

To schedule it, pass an ISO 8601 timestamp:

```ts theme={null}
const post = await soMe.posts.create(
  endUser.id,
  {
    connectionId: connection.id,
    socialMedia: "THREADS",
    postType: "TEXT",
    text: "See you tomorrow",
    scheduledAt: "2026-08-20T10:00:00Z",
  },
  { idempotencyKey: "customer_123:post_457" },
);
```

<Warning>
  Use a stable, unique idempotency key for every logical post. If your request
  times out, retry with the same key. Keys must contain 1–255 characters and are
  scoped to the end user's workspace.
</Warning>

## List and retrieve posts

```ts theme={null}
const page = await soMe.posts.list(endUser.id, {
  page: 1,
  limit: 20,
});

const post = await soMe.posts.get(endUser.id, "post_uuid");
```

`limit` is constrained to 1–100. The list response contains `data`, `page`,
`limit`, and `total`.

## Upload media

Media uploads use a three-step flow:

1. Request a presigned upload URL from Social media studio.
2. Upload the raw file bytes directly to that URL with HTTP `PUT`.
3. Attach the returned file information when creating the post.

```ts theme={null}
import { readFile, stat } from "node:fs/promises";

const filename = "launch.jpg";
const mimetype = "image/jpeg";
const size = (await stat(filename)).size;

const [media] = await soMe.posts.presignMedia(endUser.id, {
  postType: "IMAGE",
  socialMedia: "THREADS",
  files: [{ filename, mimetype, size }],
});

const upload = await fetch(media.uploadUrl, {
  method: "PUT",
  headers: { "Content-Type": mimetype },
  body: await readFile(filename),
});

if (!upload.ok) {
  throw new Error(`Media upload failed: ${upload.status}`);
}

const post = await soMe.posts.create(
  endUser.id,
  {
    connectionId: connection.id,
    socialMedia: "THREADS",
    postType: "IMAGE",
    text: "Our launch is live",
    files: [
      {
        id: media.fileId,
        filename,
        mimetype,
        size,
        s3Prefix: media.s3Prefix,
        fileSrc: media.fileSrc,
      },
    ],
  },
  { idempotencyKey: "customer_123:post_458" },
);
```

For videos, include `duration` in seconds when requesting the URL and attaching
the file. Upload URLs are temporary; request a new one if an upload has expired.

## Post status

The create response confirms that the post was accepted and queued; it does not
guarantee that the social platform published it. Use `post.status`, poll
`posts.get`, or subscribe to `post.published` and `post.failed` webhooks for the
final result.
