# Social

Publish to the project's connected Instagram, Threads and Bluesky accounts,
now or on a schedule. Accounts are connected once, by a person, from the
project's **Social** page — OAuth to a platform is not something a key can do.

Publishing is public and cannot be recalled from Pharos, so `social:publish`
is treated like the send scopes: it is never on an agent key, and the
dashboard flags any key that carries it. See
[Authentication & keys](/docs/authentication).

## `GET /api/social/posts`

The project's 50 most recent posts, newest first.

**Scope:** `social:read`

| Parameter | In | Type | Notes |
| --- | --- | --- | --- |
| `projectId` | query | string | **Required.** |

```json
{
  "posts": [
    {
      "id": "sp_…",
      "platform": "instagram",
      "content": "…",
      "mediaUrl": "https://…/promo.jpg",
      "mediaType": "image",
      "status": "posted",
      "platformPostId": "1789…",
      "scheduledAt": null,
      "postedAt": "2026-09-28T12:00:41.000Z",
      "error": null,
      "createdAt": "2026-09-28T12:00:30.000Z"
    }
  ]
}
```

`status` is `scheduled`, `posting`, `posted` or `failed`. A failed post keeps
the platform's message in `error`.

## `POST /api/social/posts`

Publish one piece of content to one or more platforms. Each platform becomes
its own post and succeeds or fails on its own.

**Scope:** `social:publish`

| Field | Type | Notes |
| --- | --- | --- |
| `projectId` | string | **Required.** |
| `content` | string | **Required.** Checked against the strictest limit among the chosen platforms — Bluesky 300, Threads 500, Instagram 2200. |
| `platforms` | string[] | **Required.** Any of `instagram`, `threads`, `bluesky`. |
| `mediaUrl` | string | **Required for Instagram**, which fetches it itself, so it must be public `https`. JPEG for images, MP4/MOV for Reels. Ignored by Threads and Bluesky, which post text only. |
| `mediaType` | enum | `image` or `video`. Inferred from the extension when omitted. |
| `scheduledAt` | string | ISO datetime. In the future, the post is parked and published by cron; in the past or omitted, it publishes now. |

```json
{
  "results": [
    {
      "platform": "instagram",
      "success": true,
      "postId": "sp_…",
      "platformPostId": "1789…"
    }
  ]
}
```

The response is `200` if any platform succeeded and `502` if none did; read
`results` either way. An Instagram video still transcoding returns
`"pending": true` — the post stays `posting` and cron finishes it. A platform
with no connected account fails with `No <platform> account connected`.
