# PostQueueHD API

Schedule and manage social video posts to Instagram, Facebook, TikTok and YouTube from one queue.

## Base URL

```
https://postqueuehd.com/api/v1
```

## Authentication

Every request needs a bearer token:

```
Authorization: Bearer pq_live_xxxxxxxx
```

Create tokens under Settings → API. The token is shown once, at creation, and stored only as a hash — if it is lost, revoke it and make another.

The API is included in paid plans. A valid token on a free plan answers `403`, not `401`: the token is fine, the plan is not.

## Errors

One shape for everything:

```json
{
  "message": "Pick at least one channel.",
  "errors": { "channels": ["Pick at least one channel."] }
}
```

| Status | Meaning |
| --- | --- |
| 401 | No token, or the token is unknown, revoked or expired |
| 403 | The token is valid but the plan does not include the API |
| 404 | No such post, or it belongs to another team |
| 422 | The payload or a plan limit rejected the request |
| 429 | Rate limited — see `Retry-After` |

## Rate limits

Per token: **60 reads a minute** and **20 writes a minute**.

## Endpoints

### GET /me

What this token can do. The team the token belongs to, its plan, and how much of each allowance is left. Call this first: it answers what you are allowed to do without having to fail a write to find out.

```bash
curl https://postqueuehd.com/api/v1/me \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### GET /channels

The channels you can publish to. Every connected channel with its provider and status. The `id` values here are what `POST /posts` expects in `channels`. A channel whose `needs_attention` is true will refuse to publish until it is reconnected.

```bash
curl https://postqueuehd.com/api/v1/channels \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### GET /posts

The queue. Posts newest-cued first, cursor-paginated. Follow `links.next` to page.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `status` | string | no | draft, scheduled, publishing, published, partially_failed, failed or cancelled |
| `per_page` | integer | no | Up to 100. Defaults to 25. |

```bash
curl "https://postqueuehd.com/api/v1/posts?status=scheduled" \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### GET /posts/{id}

One post. A single post with one target per channel, each carrying its own status. An id belonging to another team answers 404, exactly like an id that does not exist.

```bash
curl https://postqueuehd.com/api/v1/posts/42 \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### POST /posts

Schedule a post. Creates a post and one target per channel. Leave `scheduled_for` out to publish as soon as possible. Send the same `idempotency_key` twice and you get the same post back, which is what makes retrying after a timeout safe.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `caption` | string | no | Up to 5000 characters. Each platform truncates to its own limit. |
| `channels` | integer[] | yes | Channel ids from GET /channels. |
| `scheduled_for` | ISO 8601 | no | Must be in the future. Omit to publish now. |
| `media_ids` | integer[] | no | Media already uploaded to this team. |
| `idempotency_key` | string | no | Up to 64 characters. Reuse it to make a retry safe. |
| `channel_captions` | object | no | Per-channel caption override, keyed by channel id. |
| `channel_options` | object | no | Per-channel platform options, keyed by channel id. |

```bash
curl -X POST https://postqueuehd.com/api/v1/posts \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Summer drop is live",
    "channels": [1, 2],
    "scheduled_for": "2026-08-01T18:30:00Z",
    "idempotency_key": "summer-drop-1"
  }'
```

### DELETE /posts/{id}

Cancel a cued post. Cancels every channel that has not gone out yet. Anything already published is history and stays as it is; if every channel is already publishing, this answers 422.

```bash
curl -X DELETE https://postqueuehd.com/api/v1/posts/42 \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### GET /media

The media you have uploaded. Everything this team has uploaded, newest first, cursor-paginated. Transcoded variants are left out: they are built per platform and are not something you refer to. Use this to reuse a file you already sent rather than uploading it twice.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `per_page` | integer | no | Up to 100. Defaults to 25. |

```bash
curl https://postqueuehd.com/api/v1/media \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### POST /media

Start an upload. Reserves a media row and answers with somewhere to PUT the file. The bytes go to the URL that comes back, not to this endpoint, so a large video is never held in a JSON request. Your plan limits are checked here, before you send anything — a file over the limit is refused now rather than after the upload finishes. Send a sha256 `checksum` and a file already uploaded answers 200 with `already_uploaded: true` and no upload target, because there is nothing left to send.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `filename` | string | yes | The original name. Its extension decides the stored one. |
| `mime_type` | string | yes | Anything starting image/ is an image; everything else is treated as video. |
| `size_bytes` | integer | yes | Checked against the plan's video size and storage allowance before you upload. |
| `checksum` | string | no | sha256 of the file, 64 hex characters. A match reuses the existing media instead of uploading again. |

```bash
# 1. reserve the row and read the upload target
curl -X POST https://postqueuehd.com/api/v1/media \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "summer.mp4",
    "mime_type": "video/mp4",
    "size_bytes": 8412345
  }'

# 2. send the bytes where it told you to
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @summer.mp4
```

### POST /media/{id}/complete

Finish an upload. Confirms the bytes arrived and starts the probe that reads the dimensions, duration and codecs. Answers 422 if nothing was uploaded. The probe runs in the background, so this does not mean the file is usable yet — poll `GET /media/{id}` until the status is `ready`.

```bash
curl -X POST https://postqueuehd.com/api/v1/media/91/complete \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### GET /media/{id}

One piece of media. The status, and what the probe found. Status walks `uploaded` → `probing` → `ready`, or `failed` with the reason in `error`. Only `ready` media is worth scheduling: a failed file will not publish, and the `error` says why in words you can pass on to whoever supplied it.

```bash
curl https://postqueuehd.com/api/v1/media/91 \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

### DELETE /media/{id}

Delete media. Deletes the file and everything transcoded from it, freeing the storage against your plan. Media still attached to a post that has not gone out answers 422 — cancel the post first. Media whose posts have all published can go; it would be deleted by the retention window anyway.

```bash
curl -X DELETE https://postqueuehd.com/api/v1/media/91 \
  -H "Authorization: Bearer $POSTQUEUE_TOKEN"
```

## Publishing a post, start to finish

1. `GET /me` — confirm the plan and what is left of the monthly allowance.
2. `GET /channels` — take the ids you want to publish to.
3. `POST /media` — reserve a row for the file and read the upload target back.
4. `PUT` the bytes to the `upload.url` you were given, with the headers it came with.
5. `POST /media/{id}/complete` — confirm the upload and start the probe.
6. `GET /media/{id}` — poll until the status is `ready`.
7. `POST /posts` — send the caption, the channel ids, the media ids and a time.
8. `GET /posts/{id}` — read `targets[].status` to see how each channel went.

Steps 3 to 6 are only needed for a post with media. Media you have already uploaded can be reused by id, and sending the file's sha256 as `checksum` at step 3 will find it for you.

## Things worth knowing

- Uploads are probed in the background. Media is only `ready` once that finishes, and scheduling a post against media that never became ready is what makes a publish fail later for no visible reason.
- TikTok posts publish as `SELF_ONLY` until our TikTok review completes.
- YouTube has a daily upload ceiling shared across all customers; a post past it is cued for the next day rather than failed.
- Published media is deleted after the team's retention window, at most 30 days.

