# Postally API reference

The Postally REST API lets you upload media and schedule or publish posts to
every connected platform — X, Instagram, LinkedIn, Facebook, Threads, TikTok,
YouTube, and Bluesky — from your own code. All endpoints are authenticated with
an API key and return JSON.

## Base URL

```
https://app.postally.io/api/public/v1
```

Every path below is relative to this base URL, e.g.
`POST https://app.postally.io/api/public/v1/posts`.

## Authentication

Every request must include **two headers**: your API key and the brand you're
acting on.

```
Authorization: Bearer pst_live_xxxxxxxxxxxxxxxxxxxx
x-brand-id: <your_brand_id>
Content-Type: application/json
```

### Getting an API key

Generate a key in your dashboard under **Settings → API Keys**. Choose the
scopes it needs, then copy the token — it starts with `pst_live_` (or
`pst_test_`) and is shown only once.

### The x-brand-id header

A Postally account can hold several brands. The `x-brand-id` header selects
which brand every request acts on. You can find your Brand ID in the dashboard,
or by calling `GET /me`.

> **Keep your key secret.** Treat API keys like passwords. Use them only from
> your server — never expose them in browser or mobile client code. Revoke a key
> from Settings → API Keys if it's ever leaked.

## Scopes

Each API key is granted one or more scopes. A request to an endpoint without its
required scope returns `403 INSUFFICIENT_SCOPE`.

| Scope            | Grants                                                |
| ---------------- | ----------------------------------------------------- |
| `posts:read`     | List and read posts                                   |
| `posts:write`    | Create, schedule, reschedule, and delete posts        |
| `media:write`    | Upload media (files and remote URLs)                  |
| `analytics:read` | Read profile and post analytics                       |
| `brands:read`    | List brands and read account context                  |

## Account context

### `GET /me` — scope `brands:read`

Returns the organization, brand, and user resolved from your API key and
`x-brand-id` header. Handy for confirming a key and brand are wired up
correctly.

```json
{
  "organization": { "id": "org_...", "name": "Acme Inc" },
  "brand": { "id": "brand_...", "name": "Acme" },
  "user": { "id": "usr_...", "email": "you@acme.com", "name": "You" }
}
```

### `GET /brands` — scope `brands:read`

List every brand in the API key's organization. Use `GET /brands/:id` to fetch a
single brand.

## Upload media

Upload an image or video first, then attach the returned `id` and `path` to a
post. Both endpoints return the same media object.

### `POST /media/upload` — scope `media:write`

Upload a file as `multipart/form-data` in a field named `file`. Supports common
image and video formats.

```json
{
  "id": "media_...",
  "name": "launch.png",
  "path": "https://cdn.postally.io/media/launch.png",
  "thumbnail": "https://cdn.postally.io/media/launch_thumb.png",
  "alt": null
}
```

### `POST /media/upload-from-url` — scope `media:write`

Hand Postally a public URL and it downloads and stores the file for you. Returns
the same media object as above.

```json
{
  "url": "https://example.com/image.jpg"
}
```

## Publish & schedule posts

### `POST /posts` — scope `posts:write`

Create a draft, schedule a post for later, or publish immediately. A single call
can target multiple platforms — add one entry to `posts[]` per connected
account.

Top-level fields:

- `type` — one of `draft`, `schedule`, or `now`. Draft saves without publishing;
  schedule publishes at `date`; now publishes immediately.
- `date` — ISO 8601 datetime (required). Must be in the future for `schedule`.
  Ignored for `now`.
- `shortLink` — boolean (required). Auto-shorten URLs in the content.
- `posts[]` — one entry per target account (required unless `type` is `draft`).

Each `posts[]` entry:

- `integration.id` — the connected account to post to. This is what selects the
  platform.
- `value[]` — the content blocks. One block is a normal post; multiple blocks
  form a thread / multi-part post. Each block has `content` (text) and `image[]`
  (media from the upload step).
- `settings.__type` — the platform identifier; must match the integration's
  platform. Some platforms require extra fields here.
- `commentDelays[]` — optional per-block delay in minutes (0–1440) for pacing
  threads.

Request body:

```json
{
  "type": "schedule",
  "date": "2026-09-01T14:30:00Z",
  "shortLink": false,
  "posts": [
    {
      "integration": { "id": "integration_x_123" },
      "value": [
        {
          "content": "Launching something new today 🚀",
          "image": [
            {
              "id": "media_...",
              "path": "https://cdn.postally.io/media/launch.png"
            }
          ]
        }
      ],
      "settings": { "__type": "x" }
    }
  ]
}
```

Response:

```json
[
  {
    "postId": "post_...",
    "integration": "integration_x_123",
    "group": "group_..."
  }
]
```

> **Finding integration IDs.** There is no public "list connected accounts"
> endpoint yet. Get an `integration.id` from your dashboard, or read it from the
> `integration.id` field returned by `GET /posts`. Posting with an unknown ID
> returns a 400.

## Platform settings (`settings.__type`)

Set `__type` to the target platform. Most platforms need only
`{ "__type": "..." }`; a few require extra fields.

| `__type`        | Platform      | Required extra fields                                                                                                    |
| --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `x`             | X (Twitter)   | —                                                                                                                        |
| `instagram`     | Instagram     | `post_type: "post" \| "story"`                                                                                           |
| `linkedin`      | LinkedIn      | —                                                                                                                        |
| `linkedin-page` | LinkedIn Page | —                                                                                                                        |
| `facebook`      | Facebook      | —                                                                                                                        |
| `threads`       | Threads       | —                                                                                                                        |
| `tiktok`        | TikTok        | `privacy_level`, `duet`, `stitch`, `comment`, `autoAddMusic`, `brand_content_toggle`, `brand_organic_toggle`, `content_posting_method` |
| `youtube`       | YouTube       | `title` (2–100), `type: "public" \| "private" \| "unlisted"`                                                             |
| `bluesky`       | Bluesky       | —                                                                                                                        |

## Manage posts

### `GET /posts?startDate&endDate` — scope `posts:read`

List posts (scheduled and published) between two ISO 8601 dates. Each item
includes its `integration`, `state`, and `publishDate`.

### `GET /posts/:id` — scope `posts:read`

Fetch a single post, including its content blocks, attached media, platform
settings, and group.

### `PUT /posts/:id/date` — scope `posts:write`

Reschedule a post to a new time.

```json
{ "date": "2026-09-05T09:00:00Z" }
```

### `DELETE /posts/:group` — scope `posts:write`

Delete a post group (identified by the `group` returned when the post was
created). Removes all platform copies in that group.

## Analytics

### `GET /analytics/insights/profile?integrationId=` — scope `analytics:read`

Return profile-level metrics plus a list of recent posts with per-post metrics
for one connected account.

## Errors

```json
{
  "statusCode": 403,
  "code": "INSUFFICIENT_SCOPE",
  "message": "This API key is missing the required scope."
}
```

| Status | code                        | Meaning                                          |
| ------ | --------------------------- | ------------------------------------------------ |
| 401    | `API_KEY_REQUIRED`          | Missing or malformed Authorization header        |
| 401    | `INVALID_API_KEY`           | Key is invalid or revoked                        |
| 400    | `BRAND_ID_REQUIRED`         | Missing x-brand-id header                        |
| 403    | `BRAND_NOT_ALLOWED`         | Key is not permitted to act on this brand        |
| 403    | `INSUFFICIENT_SCOPE`        | Key is missing the required scope                |
| 402    | —                           | Storage limit exceeded (media upload)            |
| 400    | Platform validation failed  | Post content/settings invalid — see `issues[]`   |
| 429    | —                           | Rate limit exceeded                              |

## Rate limits

Requests are throttled per API key at roughly 60 requests per minute. Exceeding
the limit returns `429 Too Many Requests` — back off and retry.

A live OpenAPI (Swagger) reference for these endpoints is served at
https://app.postally.io/api/public/v1/docs.
