# Posts

18 endpoints for posts, with parameters and a real response example.

## GET /posts

**List posts**

Lists the workspace's posts: status, scheduled date, and the first words of the caption. Use it to find which post to act on next, since every other post endpoint needs its id. Filter by status, period, or project to narrow the search; without filters, returns the most recent.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Comma-separated list of statuses to filter by. |
| `from` | query | string | no | ISO date. Filters by scheduled date. |
| `to` | query | string | no | ISO date. Filters by scheduled date. |
| `projectId` | query | string | no |  |
| `limit` | query | integer | no | Default 25, max 100. |
| `cursor` | query | string | no | Opaque cursor from a previous response's `next` field. |

Response:

```json
{
  "posts": [
    {
      "id": "8c64bee5-f756-47b7-a64a-01f5db597202",
      "status": "draft",
      "scheduledFor": null,
      "textContent": "Seu nicho pode estar amplo demais sem você perceber. Quando a mensagem tenta alcançar todo mundo, ela deixa de ser memorável para qualquer pessoa.\nQual desses sinais aparece mai...",
      "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359",
      "contentFormat": "animated_audio",
      "theme": "Tres sinais de que o seu nicho esta amplo demais"
    },
    {
      "id": "a6eed866-eed5-4f60-b3a5-1aaba4bee2eb",
      "status": "draft",
      "scheduledFor": null,
      "textContent": "Your first SaaS customers probably aren’t waiting for an ad. They’re already dealing with a painful problem you can solve.\n\nStart with direct conversations and a simple offer. W...",
      "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359",
      "contentFormat": "animated_audio",
      "theme": "How to land your first 10 SaaS customers without spending on ads"
    }
  ],
  "next": "eyJjaGF2ZSI6IjIwMjYtMDktMTNUMDU6NTg6MzYuNDUyNTI4KzAwOjAwIiwiaWQiOiJhNmVlZDg2Ni1lZWQ1LTRmNjAtYjNhNS0xYWFiYTRiZWUyZWIifQ"
}
```

## POST /posts

**Create a post, in any of the six formats**

Creates a post from a theme and starts generating it. The `format` parameter picks which of the six formats to make, and defaults to `carrossel`. Every format costs 1 credit and takes on the order of a minute (the carousel: median 43s, p99 87s, measured in production). Fire-and-forget: responds 202 with `state: "generating"`; poll `GET /posts/{id}` for the outcome. The four formats in preview are refused with 403 unless the workspace has them enabled, and nothing is charged.

> Answers `202`. The work finishes in the background; poll the matching `GET` for the outcome.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `projectId` | body | string | yes |  |
| `theme` | body | string | no | What the post is about, in the client's words. |
| `format` | body | "carousel" | "caption_reel" | "animated_audio" | "narrated_photos" | "premium_carousel" | "infographic" | no | Which format to generate. Defaults to `carrossel`. Formats in preview (`audio_animado`, `narrated_photos`, `carrossel_profissional`, `infografico`) are refused with 403 unless the workspace has them enabled. |
| `link` | body | string | no | A URL to read before writing. When present, `theme` becomes the instruction for what to do with it. Only the `carrossel` format reads a link; any other format is refused. |

## GET /posts/{id}

**Get a post, whatever its format**

Returns one post in full, in any of the six formats. The envelope is always the same, `format` says which format it is, and `content` carries what belongs to that format: `slides` for the three carousel-shaped formats, the text blocks and look for `caption_reel`, the script and scenes for the two narrated ones. `content` is null while the post is still generating, which is a real state and not an error: read `status` to know.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

Response:

```json
{
  "id": "e7869afa-d470-4e9b-a4c9-4c1936604a4b",
  "format": "carousel",
  "status": "draft",
  "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359",
  "workspaceId": "2bd5c04d-1dd4-456e-8704-9d7b84703fd0",
  "scheduledFor": null,
  "textContent": null,
  "mediaUrls": [
    {
      "url": "https://.../post-images/ai-edits/e7869afa-d470-4e9b-a4c9-4c1936604a4b/b14c1fbe-b6f0-4ae0-bfd0-b1aaaf67a52b.webp",
      "type": "image"
    }
  ],
  "content": {
    "slides": [
      {
        "id": "1663f551-6958-41f6-bedb-e4294a9ff9a9",
        "order": 0,
        "variant": null,
        "fields": [
          {
            "name": "profissional",
            "value": {
              "blocos": [
                {
                  "peso": "etiqueta",
                  "texto": "04 · USO"
                },
                {
                  "peso": "forte",
                  "texto": "Metricaas: mede se a galera realmente usa"
                }
              ],
              "destaque": ""
            }
          }
        ],
        "titleFont": null,
        "bodyFont": null,
        "colors": {
          "background": "#fff1e2",
          "text": "#1D2939",
          "brand": "#ff6e05"
        }
      }
    ]
  }
}
```

## PATCH /posts/{id}

**Update a post, whatever its format**

Changes the caption and, on the formats that have one, the content inside the video: `reel` on `caption_reel`, `narration` on `animated_audio` and `narrated_photos`. Send only what changes; the caption is replaced in full and never merged. Sending `reel` on a post that is not a reel (or `narration` on one that is not narrated) is refused with the field name, never ignored. Only works before the post publishes.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `caption` | body | string (max 2200 chars) | no | The Instagram caption, replaced in full. Up to 2200 characters, the same ceiling Instagram has. |
| `reel` | body | object | no | Only on the `caption_reel` format: the three text blocks and the look of the video. |
| `narration` | body | object | no | Only on `animated_audio` and `narrated_photos`: the spoken script and what is drawn over it. Changing `script` does NOT re-record the audio. |

## DELETE /posts/{id}

**Delete a post**

Permanently deletes the post. Irreversible.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

## POST /posts/{id}/media

**Replace a post's media**

Replaces a single-image post's media with the given list. Does not apply to a carousel (its image is generated from its slides) or to a reel (its background video is replaced via `POST /posts/{id}/clip`). An empty list removes the media. Only works before the post publishes.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `media` | body | object[] (max 10 items) | yes | The full media list, at most 10 items (the Instagram carousel limit). An empty list removes the media. |

## POST /posts/{id}/schedule

**Schedule a post**

Schedules the post to publish on its own at a future date and time, or changes the date of a post already scheduled. Only works on a draft, a previously failed post, or one already scheduled: a post in flight (publishing, queued, generating) or already published cannot be rescheduled this way.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `when` | body | string | yes | ISO 8601 with a timezone, in the future. Either `Z` or an offset like `-03:00`. |

## POST /posts/{id}/unschedule

**Unschedule a post**

Takes the post out of the schedule queue and returns it to draft, without deleting anything. Only works on a scheduled post.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

## POST /posts/{id}/archive

**Archive a post**

Removes the post from the Posttar calendar without deleting it; `reactivate` brings it back. Does NOT remove anything from Instagram: an already published post stays published there.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

## POST /posts/{id}/reactivate

**Reactivate a post**

Takes the post out of the archive and returns it to draft, ready to edit and schedule again. Only works on an archived post.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

## POST /posts/{id}/publish

**Publish a post now**

Publishes the post to Instagram right away. Fire-and-forget: responds 202 with the post in `publishing` state (median 38s, p99 61s, measured in production); poll `GET /posts/{id}` for the outcome. Irreversible.

> Answers `202`. The work finishes in the background; poll the matching `GET` for the outcome.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

## POST /posts/{id}/slides

**Add a slide**

Appends a new slide to the end of the carousel, inheriting the last slide's layout with placeholder text. Edit that text afterwards with `PATCH /slides/{id}`. Carousel only.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `layoutId` | body | string | no | Without it, inherits the layout of the post's last slide. |

## POST /posts/{id}/slides/order

**Reorder slides**

Changes the order of the carousel's slides. Send the complete list of slide ids in the new order; a missing or repeated id is rejected, because the order cannot have a gap.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `order` | body | string[] (max 50 items) | yes | The complete list of slide ids, in the new order. |

## GET /posts/{id}/edit-budget

**Get a post's image-edit budget**

Returns how many AI image edits (`POST /slides/{id}/reimagine`) this post has used and how many remain, so a client can check before spending one (about $0.015 each, capped at 2 on the standard plan) instead of risking a call just to find the budget empty.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

Response:

```json
{
  "used": 1,
  "cap": 2,
  "remaining": 1
}
```

## GET /posts/{id}/clips

**List a reel's available clips**

Lists the background clips available to this reel from the project's video library: id and description of each, before choosing one with `POST /posts/{id}/clip`.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

Response:

```json
{
  "clips": []
}
```

## POST /posts/{id}/clip

**Set a reel's background clip**

Replaces the reel's background video with a clip from the project's video library. `clipId` must come from `GET /posts/{id}/clips`.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `clipId` | body | string | yes |  |

## GET /posts/{id}/tracks

**List a reel's available tracks**

Lists the music tracks this reel can use: name, description, duration, which one is currently in use, and a link to listen to each, before choosing one with `POST /posts/{id}/track`.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |

Response:

```json
{
  "tracks": [
    {
      "id": "94917ea8-17e6-47de-a2df-c79146cb5771",
      "name": "Bad Bunny - MONACO (Instrumental) - TYPE",
      "description": null,
      "durationSeconds": 267.377778,
      "suggestedStartSeconds": 19,
      "url": "https://.../audio-tracks/posttar/bad-bunny-monaco-instrumental-type.mp3",
      "inUse": false
    },
    {
      "id": "b6bb3c6d-9324-4df2-aced-70a6aead9a55",
      "name": "INSONAMIA - Ronald Figo",
      "description": null,
      "durationSeconds": 181.36815,
      "suggestedStartSeconds": 15,
      "url": "https://.../audio-tracks/posttar/insonamia-ronald-figo.mp3",
      "inUse": false
    }
  ],
  "inUse": "fda25fc1-6f3f-4eb6-b7d5-c53501a8c077"
}
```

## POST /posts/{id}/track

**Set a reel's track**

Sets, changes, or removes the reel's music. `trackId` must come from `GET /posts/{id}/tracks`; null mutes the reel. `startSeconds` picks which second of the track the reel's 7 seconds start from.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Opaque id from a previous response. |
| `trackId` | body | string | null | yes | From GET /posts/:id/tracks. Null mutes the reel. |
| `startSeconds` | body | number | no | What second of the track the reel's 7 seconds start from. |
