# Posttar API The complete public API documentation, in one file. Base URL: https://api.posttar.com/v1 OpenAPI: https://posttar.com/docs/api/openapi.json --- # Posttar API Automate your own Posttar account: create and edit content, schedule, publish, run autopilot and read reports, without opening the app. The Posttar API gives a script, or another system, the same reach the app has over your account. You can create a post from a theme, edit its slides, set media, schedule it, publish it, turn autopilot on, and read how it performed. It is available on the **Plus** and **Scale** plans. Calls spend the credits your plan already includes, with no separate billing. ## Base URL ```text https://api.posttar.com/v1 ``` That host serves the API and nothing else: any other path on it answers `404` in the same JSON error shape, never HTML. The same endpoints also answer at `https://app.posttar.com/api/v1`, which is the internal name of the same route. ## What you can do | Area | What the API covers | | --- | --- | | Posts | Create in any of the six formats, read, edit, set media, archive, delete | | Slides | Read, add, edit, reorder, remove, swap layout, reimagine the image | | Scheduling | Schedule, unschedule, publish now | | Reel media | List and pick the background clip and the music track | | Autopilot | Turn on and off, manage slots and their directives | | Projects | List, create, rename | | Brand DNA | Read, list and apply palettes, change the brand photo and logo | | Radar | Read the latest analysis and the saved inspirations | | Reports | Period summary and per-post performance | | Team | List who is in the workspace | > Connecting Instagram, paying, and changing plan do not cross into the API: all three are OAuth or checkout flows that need a browser. See the `excluded` list in the OpenAPI document for everything that stays out and why. ## Built for agents Every page here answers in plain Markdown at the same URL with `.md` at the end, so an agent can fetch it without parsing HTML. There is also [/llms.txt](/llms.txt) for the index and [/docs/api/llms-full.txt](/docs/api/llms-full.txt) for this entire documentation in one file. The machine contract is [the OpenAPI document](/docs/api/openapi.json), generated from the same registry that serves the requests, so it cannot drift from what the API actually does. --- # Authentication Every call carries an Authorization header with your API key. Every call carries an `Authorization` header with your key. ```text Authorization: Bearer ptr_live_a1b2c3d4_1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f ``` ## Getting a key In the app, under **Settings → API & MCP**. Only the workspace owner or an admin can create one. > The secret is shown once, at the moment you create it. After that we keep only a hash, and no screen or support person can recover a lost key. If you lose it, revoke the old one and create another. ## What a key can reach A key belongs to one workspace and sees everything that workspace has, with the same permissions as the person who created it. There is no finer scope yet, such as a read-only key. Creating, listing and revoking keys happens only in the app, never through the API itself. A key that could mint keys would survive its own revocation, which is the one thing a leaked credential must not do. ## When authentication fails | Situation | Status | code | | --- | --- | --- | | No header at all | `401` | `unauthenticated` | | Key is malformed, unknown or revoked | `401` | `unauthenticated` | | Key is valid but the plan does not include the API | `403` | `forbidden` | A call with no key: ```json { "error": { "code": "unauthenticated", "message": "missing Authorization: Bearer " } } ``` --- # Your first call List the posts in your workspace, which is where every other call starts. Listing posts is the natural first call: every other post endpoint needs an id, and this is where ids come from. ```bash curl -H "Authorization: Bearer ptr_live_..." \ "https://api.posttar.com/v1/posts?limit=2" ``` A real response, trimmed to two posts: ```json { "posts": [ { "id": "8c64bee5-f756-47b7-a64a-01f5db597202", "status": "draft", "scheduledFor": null, "textContent": "Seu nicho pode estar amplo demais sem você perceber...", "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359", "contentFormat": "animated_audio", "theme": "Tres sinais de que o seu nicho esta amplo demais" } ], "next": "eyJjaGF2ZSI6IjIwMjYtMDktMTNUMDU6MTg6MDYuOTE4Njcz..." } ``` ## Narrowing the list | Query parameter | What it does | | --- | --- | | `status` | Comma-separated: `draft,scheduled,published,failed,idea` | | `from` and `to` | ISO dates, filtering by scheduled date | | `projectId` | One project, when the workspace has more than one | | `limit` | Default 25, max 100 | | `cursor` | The `next` value from the previous response | > With `from` or `to` the list is ordered by scheduled date, ascending. Without them it is ordered by creation, newest first. It is the same rule the app's calendar uses. --- # Errors Every error has the same shape, and the code is the stable part. Every error, from every endpoint, has the same shape. ```json { "error": { "code": "not_found", "message": "Post nao encontrado" } } ``` `code` is the stable contract: branch your code on it. `message` is human text meant to be shown or logged, it is written in Portuguese, and it can change without notice. Never match on it. | `code` | HTTP | What happened | | --- | --- | --- | | `unauthenticated` | `401` | No key, or a key that is malformed, unknown or revoked | | `forbidden` | `403` | The plan does not include the API, or the role cannot do this | | `not_found` | `404` | The resource does not exist, or is not in this workspace | | `invalid_input` | `400` | A field is missing, malformed or out of range | | `invalid_state` | `409` | The resource exists but is in a state where this cannot happen | | `rate_limited` | `429` | Too many requests, see Rate limits | | `upstream_failure` | `500` | Something on our side failed | > A resource in another workspace answers `404`, never `403`. Telling the difference would confirm that the id exists somewhere, which is itself a leak. ## Method not allowed A path that exists but not with that verb answers `405` with an `Allow` header listing what it does accept. The body is the same error shape, so a client never has to special-case it. --- # Pagination Cursor pagination on the three listings that can grow without bound. Three listings can grow without bound and take a cursor: `GET /posts`, `GET /projects/:id/reports/performance` and `GET /projects/:id/radar/inspirations`. Every other listing has a fixed ceiling, by business rule or by plan, and needs no cursor. The cursor is opaque. Do not read one or build one: pass back in `cursor` exactly what came in the `next` field of the previous response. When `next` comes back `null`, the pages are over. ```bash curl -H "Authorization: Bearer ptr_live_..." \ "https://api.posttar.com/v1/posts?limit=2&cursor=eyJjaGF2ZSI6IjIwMjYtMDktMTNUMDU6MTg6MDYuOTE4Njcz..." ``` > A malformed cursor is refused with `invalid_input`. It never silently falls back to page one, which would make a `while (next)` loop spin forever on the same rows. --- # Rate limits 60 requests per minute per key, best effort. **60 requests per minute, per key.** This is best effort, not a guarantee: the counter lives in the memory of each server instance, and the same key can land on different instances and in practice go a little past 60 before being refused. The error always leans toward letting more calls through, never toward refusing a legitimate one. If your use depends on an exact ceiling, do not treat this number as a hard limit. The real ceiling is your plan's credit balance. Going over answers `429`, with a `Retry-After: 60` header: ```json { "error": { "code": "rate_limited", "message": "too many requests: the limit is 60 requests per minute per API key" } } ``` --- # Long-running calls Two operations answer 202 and finish in the background. Two operations start heavy work and answer before it finishes: creating a post, in any of the six formats, and publishing. They answer **202**, not 200, because Posttar does not pretend the work is done. The resource comes back in its real state, and you fetch the outcome later with a `GET`. | Operation | Median | p99 | | --- | --- | --- | | `POST /posts` (`carrossel`) | 43 s | 87 s | | `POST /posts` (`leia_a_legenda`) | 40 to 70 s in the common case | | | `POST /posts` (`audio_animado`) | around 1 minute | | | `POST /posts` (`narrated_photos`) | around 2 minutes | | | `POST /posts/:id/publish` | 38 s | 61 s | Measured in production. While a post is not finished, `status` stays `queued` or `generating`. When it finishes it becomes `draft`, ready for review, or `failed`. Poll `GET /posts/:id` until it leaves those two states. > There is no outcome webhook in this version. Polling is the only way to learn that a long call finished. ## Create a post and wait for it The whole sequence, in order. Copy this block if you are handing the flow to an agent. ### 1. Start the creation. Answers 202. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"three signs your niche is too broad"}' \ "https://api.posttar.com/v1/posts" ``` ```json { "postId": "dd8d142c-4cba-4ea6-9088-e948ac347f56", "format": "carrossel", "state": "generating", "remainingCredits": 5 } ``` ### 2. Poll until status leaves queued and generating. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" \ "https://api.posttar.com/v1/posts/dd8d142c-4cba-4ea6-9088-e948ac347f56" ``` ```json { "id": "dd8d142c-4cba-4ea6-9088-e948ac347f56", "status": "generating", "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359", "workspaceId": "2bd5c04d-1dd4-456e-8704-9d7b84703fd0", "scheduledFor": null, "textContent": null, "mediaUrls": [] } ``` > Creating answers `state`, reading answers `status`. They are two different fields of two different shapes: `state` only ever says `"generating"`, because that is all the creation call knows, while `status` carries the real lifecycle (`queued`, `generating`, `draft`, `scheduled`, `published`, `failed`). Read what the specific call returns instead of assuming similar fields share values. --- # Field language Paths, request fields, response fields and almost every value are English. Paths and field names are English on both sides: what you **send** (`projectId`, `scheduledFor`, `when`) and what you **receive** (`projects`, `name`, `createdAt`). This was not always true. Until 13/09/2026 the response fields came out in Portuguese, and until 14/09/2026 so did the error envelope, because the internal layer that builds them was written before the API existed. They were renamed at the source, not translated at the boundary: a translation layer means two tables to keep in step, and the one nobody updates is where the bug hides. A test now refuses any Portuguese field in a response example. Checked against the 14 endpoints that carry a response example: none of them returns a Portuguese field name. ## Values too Values are English as well: `format` says `caption_reel`, `status` says `generating`, `alignment` says `center`, `dayName` says `monday`, an audiogram scene's `type` says `bar_chart`, and the error `code` says `not_found`. > This page claimed on the morning of 14/09/2026 that nothing was Portuguese, and it was wrong: the six `format` values were still `carrossel`, `leia_a_legenda`, `audio_animado`, `carrossel_profissional` and `infografico` — the most-typed values in the whole API, in the table two pages over. The test that was supposed to catch it compares field NAMES in response examples, so it stayed green with `audio_animado` in plain sight. Measuring the wrong thing feels exactly like being done. ## The two places that are still Portuguese, and why | Where | What you will see | Why it stays | | --- | --- | --- | | `message` in an error | `Post nao encontrado` | Human text, meant to be shown or logged. The stable contract is `code`, which is English. | | `content.slides[].fields[].value` | `blocos`, `peso`, `texto` | The slide's own content, shaped by its layout template. You send the object back unchanged; the API never reads inside it. | ## Slide content passes through untouched In `GET /posts/:id/slides` every field comes as `{ name, value }`. The `value` is the content of that slide's layout, its shape comes from the template, and you send the whole object back unchanged in `PATCH /slides/:id`. The API guarantees the language of the envelope it builds, not of the content passing through it. ## Error messages The envelope is `{ "error": { "code", "message" } }`. `code` is the stable contract, in English. `message` is human text, written in Portuguese, and it can change wording without notice: safe to show a person, unsafe to match on. See [Errors](/docs/api/errors). --- # Post types The six formats, all created by the same endpoint, with a call for each. Posttar has six content formats, and one endpoint creates all of them: `POST /posts`, with a `format` parameter. Leave `format` out and you get `carousel`, the plain carousel. | `format` | What it is | Media | | --- | --- | --- | | `carousel` | Carousel: image slides with a caption | Image carousel | | `caption_reel` | Read-the-caption reel: three text blocks over a clip | Short video | | `animated_audio` | Animated audio reel: the project voice narrates, over a waveform | Short video | | `narrated_photos` | Narrated photos reel: the project voice narrates, one AI photo per passage | Short video | | `premium_carousel` | Premium carousel: six pages drawn whole by the model | Image carousel | | `infographic` | Infographic: one vertical piece drawn whole by the model | Short video, silent | > The four preview formats (`animated_audio`, `narrated_photos`, `premium_carousel`, `infographic`) are enabled per workspace. Calling one the workspace does not have answers `403` with `forbidden`, and nothing is charged. `carousel` and `caption_reel` are open to everyone. ## One call per format Every one of them costs 1 credit, answers `202`, and finishes in the background. Poll `GET /posts/:id` until `status` leaves `queued` and `generating`. Carousel. The default, so `format` is optional here. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"three signs your niche is too broad"}' \ "https://api.posttar.com/v1/posts" ``` Read-the-caption reel. The background clip is chosen by Posttar, and the response says which source it used. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"why your niche is too broad", "format":"caption_reel"}' \ "https://api.posttar.com/v1/posts" ``` Animated audio reel. Needs a voice on the project; without one the call is refused with `invalid_state` and the message says where to record it. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"how i price my work","format":"animated_audio"}' \ "https://api.posttar.com/v1/posts" ``` Narrated photos reel. Same voice requirement, one AI photo per passage instead of the waveform. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"my first client","format":"narrated_photos"}' \ "https://api.posttar.com/v1/posts" ``` Premium carousel. Six pages drawn whole, instead of text over a template. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"the 5 stages of a launch", "format":"premium_carousel"}' \ "https://api.posttar.com/v1/posts" ``` Infographic. One vertical piece, published as a silent reel so it keeps video distribution. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"freelance rates by years of experience", "format":"infographic"}' \ "https://api.posttar.com/v1/posts" ``` The answer has the same shape for all six. `format` echoes what was created, and `clipSource` appears only for `caption_reel`, which is the only format with a background clip. ```json { "postId": "dd8d142c-...", "format": "caption_reel", "state": "generating", "clipSource": "library", "remainingCredits": 5 } ``` ## The two limits worth knowing before you call - `theme` is capped at 10,000 characters, except on `animated_audio` and `narrated_photos`, where it stops at 500: there the theme becomes a spoken script. - `link` (a page for Posttar to read before writing) works only on `carousel`. With any other format the call is refused instead of ignoring it, because ignoring it would spend a credit on a post about the wrong subject. ## What is editable after it is created Every format is readable, schedulable, publishable and deletable through the API. Format-specific editing is uneven: the carousel has slide endpoints, the read-the-caption reel has `PATCH /reels/:id` plus clip and track endpoints, and the animated audio reel has `PATCH /audiograms/:id`. The other three have no editing endpoints yet, so a post of those formats is create-then-publish through the API, or editable in the app. --- # Create a post From a theme to a draft you can review, with the polling loop. Creating is asynchronous. You send a theme, get `202` with an id, and poll until the post leaves the working states. Median 43 s, p99 87 s, measured in production. You need a `projectId`. If you do not have one, `GET /projects` lists them. ## Theme to draft The whole sequence. This block is meant to be handed to an agent as is. ### 1. Find the project you are writing for. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/projects" ``` ```json { "projects": [ { "id": "c85848ba-...", "name": "My brand", "instagram": null, "createdAt": "2026-07-31T14:29:14Z" } ], "limit": 1, "canCreateMore": false } ``` ### 2. Start the post. Answers 202, the work runs in the background. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"projectId":"c85848ba-...","theme":"three signs your niche is too broad"}' \ "https://api.posttar.com/v1/posts" ``` ```json { "postId": "dd8d142c-...", "format": "carousel", "state": "generating", "remainingCredits": 5 } ``` ### 3. Poll until status is no longer queued or generating. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/posts/dd8d142c-..." ``` ```json { "id": "dd8d142c-...", "status": "draft", "projectId": "c85848ba-...", "workspaceId": "2bd5c04d-...", "scheduledFor": null, "textContent": "Your niche might be broader than you think...", "mediaUrls": [{ "url": "https://.../slide-1.webp", "type": "image" }] } ``` ### 4. Read the slides, if you want to edit them before publishing. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/posts/dd8d142c-.../slides" ``` ## Writing from a link `POST /posts` also takes a `link`. When you send one, Posttar reads that page first, and `theme` becomes the instruction for what to do with it ("turn this into three tips", for example). You can send either one alone, but not neither. ## What can go wrong | `code` | What it means here | | --- | --- | | `invalid_input` | Neither `theme` nor `link` was sent, or `projectId` is not a valid id | | `not_found` | The project is not in this workspace | | `invalid_state` | No credits left on the plan | > Credits are spent when generation starts, not when it succeeds. A post that ends in `failed` has already cost its credit. --- # Edit a post Change the caption, the media, and the slides one by one. A post has three editable surfaces, and they are separate endpoints: the caption, the media list, and the slides. ## The caption ```bash curl -X PATCH -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"caption":"the new caption"}' \ "https://api.posttar.com/v1/posts/dd8d142c-..." ``` Limited to 2200 characters, the same ceiling Instagram has. ## The slides `GET /posts/:id/slides` returns them in order, each with its id, its layout and its text fields. From there: - `PATCH /slides/:id` changes the text of one slide - `POST /slides/:id/variant` swaps its layout for another - `POST /slides/:id/reimagine` regenerates its image - `POST /posts/:id/slides` adds one at the end - `DELETE /slides/:id` removes one - `POST /posts/:id/slides/order` reorders the whole set > Image changes are metered. `GET /posts/:id/edit-budget` tells you how many you have used and how many are left on that post, as `used`, `cap` and `remaining`. ## Rewrite the first slide and reorder Read, edit one, then set the order explicitly. ### 1. Read the slides to get their ids and current order. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/posts/dd8d142c-.../slides" ``` ### 2. Change the text of one slide. ```bash curl -X PATCH -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"fields":{"title":"A sharper hook"}}' \ "https://api.posttar.com/v1/slides/9f2a1c70-..." ``` ### 3. Set the order. Send the complete list of slide ids, in the new order. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"order":["9f2a1c70-...","2b7d4e91-...","c1a8f350-..."]}' \ "https://api.posttar.com/v1/posts/dd8d142c-.../slides/order" ``` ## The media `POST /posts/:id/media` replaces the post's media list with the one you send, in a `media` field. At most 10 items, which is the Instagram carousel limit, and an empty list removes the media. Use it when you render images yourself and want Posttar to publish those instead of the generated ones. --- # Schedule and publish Put a post on the calendar, take it off, or publish it right now. A draft becomes a scheduled post with one call, and scheduling is reversible until it fires. Schedule: ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"when":"2026-10-02T13:00:00Z"}' \ "https://api.posttar.com/v1/posts/dd8d142c-.../schedule" ``` Take it off the calendar, back to draft: ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" \ "https://api.posttar.com/v1/posts/dd8d142c-.../unschedule" ``` ## Publishing now `POST /posts/:id/publish` publishes to Instagram immediately. It answers `202`: the post comes back as `publishing` and finishes in the background. Median 38 s, p99 61 s. > Publishing needs a connected Instagram account on the project. Connecting it is OAuth and does not cross into the API, so a project that was never connected in the app cannot publish through it either. ## The states a post moves through | status | What it means | | --- | --- | | `idea` | A theme captured, nothing generated yet | | `queued`, `generating` | Being created right now | | `draft` | Ready for review | | `scheduled` | On the calendar, waiting for its time | | `publishing` | Being sent to Instagram | | `published` | Live on Instagram | | `failed` | Generation or publication failed | | `archived` | Put away, still readable | > `POST /posts/:id/archive` does not remove anything from Instagram. A post that is already published stays published there. --- # Read and edit One call reads any post, one call edits it, whatever its format. `GET /posts/:id` reads any of the six formats and `PATCH /posts/:id` edits it. You do not need to know the format beforehand: the answer tells you, in `format`, and puts what belongs to that format in `content`. | `format` | What `content` carries | What you can change | | --- | --- | --- | | `carousel` | `slides`: each slide's id, layout and text fields | The caption, plus the slide endpoints | | `caption_reel` | The three text blocks, font, alignment, colors, sizes, clip and track | The caption and `reel` | | `animated_audio` | `script`, `hook`, `speed` and the illustrated `scenes` | The caption and `narration` | | `narrated_photos` | `script`, `hook`, `speed` and the photo `scenes` | The caption and `narration` | | `premium_carousel` | `slides`: six AI-drawn pages | The caption, plus `POST /slides/:id/reimagine` | | `infographic` | `slides`: one AI-drawn piece | The caption, plus `POST /slides/:id/reimagine` | > `content` comes back `null` while the post is still generating. That is a real state, not an error: the post exists and its content does not exist yet. Read `status`. ## Reading ## Read a post you just created The same two calls work for every format. Nothing here branches on what you made. ### 1. Read it. `format` tells you what it is. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/posts/b713e349-..." ``` ```json { "id": "b713e349-...", "format": "caption_reel", "status": "draft", "projectId": "c85848ba-...", "textContent": "Your niche is not the topic you like...", "mediaUrls": [{ "url": "https://.../out.mp4", "type": "video" }], "content": { "blocks": ["Your niche is not the topic you like", "it is the problem you can solve again", "Answer the 3 questions in the caption"], "frames": [0, 60, 150], "font": "Inter", "alignment": "center", "boxStyle": "rounded" } } ``` ## Editing `caption` works on all six. `reel` only on `caption_reel`, `narration` only on the two narrated ones. Sending the wrong one is refused with the field's name in the message, never ignored: ignoring it would let you believe you had saved. Change the caption and the reel's hook in one call. ```bash curl -X PATCH -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"caption":"the new caption", "reel":{"block1":"A sharper hook"}}' \ "https://api.posttar.com/v1/posts/b713e349-..." ``` Change a narrated script. It does NOT re-record the audio: the video keeps the voice that is already there, and the post is flagged as having stale media. ```bash curl -X PATCH -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"narration":{"script":"[calm] Look at this. [break] Three things change."}}' \ "https://api.posttar.com/v1/posts/8c64bee5-..." ``` > A scene is `{ start, end, type, fields }`, and each `type` only accepts the fields it draws. Read the current ones in `content.scenes` and send the list back complete: it replaces, it does not merge. The [Reference](/docs/api/reference/posts) lists all thirteen types. ## The reel's clip and track The background clip and the music are their own catalogs, so they have their own endpoints: `GET /posts/:id/clips` and `GET /posts/:id/tracks` list what this post can use, and `POST /posts/:id/clip` and `POST /posts/:id/track` pick one. They only apply to `caption_reel`. `startSeconds` is optional, and `trackId: null` mutes the reel. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"trackId":"94917ea8-...","startSeconds":19}' \ "https://api.posttar.com/v1/posts/b713e349-.../track" ``` --- # Autopilot Let Posttar create and publish on a weekly schedule. Autopilot creates and publishes on its own, on a weekly grid of slots. Each slot is a weekday (`dayOfWeek`, 0 for Monday), a whole hour in the project's timezone (`hour`), and an optional `theme` for what to write about. Without a theme, autopilot picks the topic. Read the current grid: ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/projects/c85848ba-.../autopilot" ``` ```json { "projectId": "c85848ba-...", "enabled": false, "emailReport": true, "requiresApproval": false, "timezone": "America/Sao_Paulo", "slots": [], "blockers": ["no_slots", "no_instagram"] } ``` `blockers` is the useful field: it lists what is stopping autopilot from running. `no_slots` means the grid is empty, `no_instagram` means the project has no connected account, `no_subscription` means the plan lapsed. Turning it on while a blocker stands will not make it publish. ## Set up a weekly schedule Add slots, then turn it on. ### 1. Add a slot: Tuesday at 09:00. `dayOfWeek` counts from Monday as 0. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" -H "Content-Type: application/json" \ -d '{"dayOfWeek":1,"hour":9,"theme":"a practical tip, no jargon"}' \ "https://api.posttar.com/v1/projects/c85848ba-.../autopilot/slots" ``` ### 2. Turn autopilot on. ```bash curl -X POST -H "Authorization: Bearer $POSTTAR_KEY" \ "https://api.posttar.com/v1/projects/c85848ba-.../autopilot/enable" ``` ### 3. Check that no impediment is left. ```bash curl -H "Authorization: Bearer $POSTTAR_KEY" "https://api.posttar.com/v1/projects/c85848ba-.../autopilot" ``` > With `requiresApproval` on, autopilot creates the post and stops at `draft` instead of publishing. Use it when you want a human to look before anything goes out. --- # Reports What was published in a period, and how each post performed. Two endpoints, and they answer different questions. `GET /projects/:id/reports/summary` counts what exists. `GET /projects/:id/reports/performance` lists each post with its metrics. The summary, for a project with nothing published yet: ```json { "period": { "from": null, "to": null }, "published": 0, "scheduled": 0, "drafts": 21, "publishedWithoutMetrics": 0, "publishedWithoutDate": 0 } ``` Both take `from` and `to` as ISO dates. Without them the whole history is considered. ## Posts with no metrics `publishedWithoutMetrics` and the `withoutMetrics` list are not a bug in the report: they are posts that were published but whose Instagram metrics were never collected. A post published outside Posttar, or published before the account was connected, lands there. > `performance` is one of the three listings that take a cursor. With a long history, read it page by page using the `next` field. --- # 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. | --- # Slides 5 endpoints for slides, with parameters and a real response example. ## PATCH /slides/{id} **Update a slide's fields** Changes one or more text fields of a single slide: title, body, or any other text field the slide's layout has. Use `GET /posts/{id}/slides` first to learn the field names and current values, and send them exactly as they appeared there. Does not change color, font, texture (`POST /slides/{id}/style`) or the photo. Carousel only. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `fields` | body | object | yes | Field name to new value, only for fields the slide's layout accepts. Values are text, up to 2200 characters each, at most 20 fields per request. | ## DELETE /slides/{id} **Delete a slide** Removes a slide from the carousel and renumbers the rest. Deleting the only remaining slide is rejected: a carousel needs at least one. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | ## POST /slides/{id}/style **Update a slide's visual style** Changes the font, colors (background, text, brand), or background texture of a single slide, never the whole post and never the project's brand. Send only the fields that need to change; colors must be hex (#rrggbb). | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `titleFont` | body | string | no | | | `bodyFont` | body | string | no | | | `backgroundColor` | body | string | no | Hex, e.g. #112233. | | `textColor` | body | string | no | Hex, e.g. #112233. | | `brandColor` | body | string | no | Hex, e.g. #112233. | | `texture` | body | string | no | | ## POST /slides/{id}/variant **Change a slide's layout variant** Swaps a single slide's layout variant for another one from the catalog. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `layoutId` | body | string | yes | | ## POST /slides/{id}/reimagine **Reimagine a slide with AI** Only for the formats whose slide is a finished image (`premium_carousel` and `infographic`), where the whole slide is AI-designed and the text is baked into the image, so there is no field for `PATCH /slides/{id}` to change. Describe the change in one sentence and the slide is redesigned, preserving the rest. Limited edits per carousel; `GET /posts/{id}/edit-budget` reports how many remain. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `instruction` | body | string | yes | The change, in one sentence. | --- # Autopilot 6 endpoints for autopilot, with parameters and a real response example. ## GET /projects/{id}/autopilot **Get a project's autopilot schedule** Returns the autopilot's weekly grid: whether it is on, which days and hours are set, each slot's fixed theme, and the project's timezone, plus any blockers that would prevent publication even while on. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | Response: ```json { "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359", "enabled": false, "emailReport": true, "requiresApproval": false, "timezone": "America/Porto_Velho", "slots": [], "blockers": [ "no_slots", "no_instagram" ] } ``` ## POST /projects/{id}/autopilot/enable **Turn on autopilot** Turns on the autopilot: Posttar starts generating and scheduling posts on its own at the grid's slots. Rejected when the grid is empty. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | ## POST /projects/{id}/autopilot/disable **Turn off autopilot** Turns off the autopilot. No slot is deleted: the grid is kept and comes back in full when turned on again. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | ## POST /projects/{id}/autopilot/slots **Add an autopilot slot** Adds one more day and hour to the autopilot's grid, optionally with a fixed theme. Does not turn the autopilot on by itself. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `dayOfWeek` | body | integer (0 to 6) | yes | 0 is Monday, 6 is Sunday. | | `hour` | body | integer (0 to 23) | yes | Whole hour, in the project's timezone. | | `theme` | body | string | no | Fixed topic for this slot. Without it, the autopilot picks the topic. | ## PATCH /projects/{id}/autopilot/slots/{slotId} **Update an autopilot slot's theme** Changes a slot's fixed theme. A blank value erases it, and the autopilot goes back to choosing the topic on its own for that slot. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `slotId` | path | string | yes | Opaque id from a previous response. | | `theme` | body | string | yes | Blank erases the fixed topic: the autopilot goes back to choosing it. | ## DELETE /projects/{id}/autopilot/slots/{slotId} **Delete an autopilot slot** Removes a slot from the autopilot's grid. Does not turn the autopilot off and does not touch other slots. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `slotId` | path | string | yes | Opaque id from a previous response. | --- # Projects 3 endpoints for projects, with parameters and a real response example. ## GET /projects **List projects** Lists the account's projects, with each one's Instagram handle, how many projects the plan allows, and whether another one still fits. Response: ```json { "projects": [ { "id": "c85848ba-2881-4a2e-9bdd-841b26d07359", "name": "Projeto de teste", "instagram": null, "createdAt": "2026-07-31T14:29:14.237659+00:00" } ], "limit": 1, "canCreateMore": false } ``` ## POST /projects **Create a project** Creates a new project. A project is an Instagram account with its own brand. Born without brand DNA and without Instagram connected; rejected when the plan's project limit is already reached. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | | | `timezone` | body | string | no | IANA, e.g. America/Sao_Paulo. Without it, born in Sao Paulo. | ## PATCH /projects/{id} **Rename a project** Changes a project's display name in the app. Does not touch the profile name inside the brand DNA, which signs the posts' artwork. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `name` | body | string | yes | | --- # Brand DNA 6 endpoints for brand dna, with parameters and a real response example. ## GET /projects/{id}/dna **Get a project's brand DNA** Returns the project's brand DNA: business description, target audience, content preferences, tone of voice, image style, title and body fonts, and the two brand colors, together with the catalog of fonts and image styles this project can choose from, which is where the valid values for `PATCH /projects/{id}/dna` come from. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `locale` | query | "pt" | "en" | "es" | no | | Response: ```json { "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359", "projectName": "Projeto de teste", "description": "A Posttar é uma ferramenta da VINTTI Soluções Digitais que automatiza a criação, personalização, agendamento e publicação de carrosséis no Instagram. Gera textos e imagens, revi...", "audience": "Pessoas, marcas e empresas que querem manter constância no Instagram, mas não têm tempo, equipe ou conhecimento para criar carrosséis. Fala especialmente com empreendedores, neg...", "contentPreferences": "Priorizar conteúdos práticos sobre crescimento no Instagram, constância, automação, criação de carrosséis, identidade visual, geração de leads e produtividade. Mostrar como a fe...", "toneOfVoice": "Escreva em português, de forma direta, simples e comercial, com frases curtas e foco no benefício. Use linguagem acessível, evitando excesso de termos técnicos. Fale com quem qu...", "imageStyle": "modern", "titleFont": null, "bodyFont": null, "colors": { "brand": "#3C50DD", "background": "#FDFDFF" }, "options": { "fonts": [ "Inter", "Poppins", "Sora", "Manrope", "Space Grotesk", "Fraunces", "Instrument Serif", "Bebas Neue", "JetBrains Mono", "Permanent Marker", "Caveat" ], "imageStyles": [ { "key": "journalistic", "name": "Jornalístico" }, { "key": "illustration", "name": "Ilustração" } ] } } ``` ## PATCH /projects/{id}/dna **Update a project's brand DNA** Saves only the fields sent; anything omitted stays exactly as it is. Text fields are replaced in full, never merged; an empty text field is rejected. Colors must be hex. Applies to what Posttar generates from now on: no existing post, caption, or slide text is rewritten, and an already published post never changes. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `description` | body | string | no | | | `targetAudience` | body | string | no | | | `contentPreferences` | body | string | no | | | `toneOfVoice` | body | string | no | | | `imageStyle` | body | string | no | A key from GET /projects/:id/dna's `opcoes.estilosDeImagem`. | | `titleFont` | body | string | no | A font from GET /projects/:id/dna's `opcoes.fontes`. | | `bodyFont` | body | string | no | A font from GET /projects/:id/dna's `opcoes.fontes`. | | `brandColor` | body | string | no | Hex, e.g. #112233. | | `backgroundColor` | body | string | no | Hex, e.g. #112233. This is the posts' background. | ## GET /projects/{id}/palettes **List a project's color palettes** Posttar's ready-made color palettes, each with the key `POST /projects/{id}/palettes/apply` accepts, plus the colors this project uses today. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | Response: ```json { "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359", "ready": [ { "key": "verde", "brandColor": "#099250", "schemes": [ { "name": "Padrão", "brandColor": "#099250", "backgroundColor": "#F6FEF9", "textColor": "#1D2939" }, { "name": "Fundo escuro", "brandColor": "#3CCB7F", "backgroundColor": "#052E1C", "textColor": "#FFFFFF" } ] }, { "key": "teal", "brandColor": "#0E9384", "schemes": [ { "name": "Padrão", "brandColor": "#0E9384", "backgroundColor": "#F6FEFC", "textColor": "#1D2939" }, { "name": "Fundo escuro", "brandColor": "#2ED3B7", "backgroundColor": "#0A2926", "textColor": "#FFFFFF" } ] } ], "current": [ { "id": "7c927623-f8c3-4f7a-8d5c-94a0d0430281", "name": "Padrão", "brandColor": "#3C50DD", "backgroundColor": "#FDFDFF", "textColor": "#1D2939", "hasLogo": false } ], "usesMoreThanOnePalette": false } ``` ## POST /projects/{id}/palettes/apply **Apply a color palette** Replaces the brand's colors with one of the ready-made palettes (or the project's own brand color hex), preserving the logo. Applies to what Posttar generates from now on; an already published post never changes. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `key` | body | string | yes | From GET /projects/:id/palettes, either the preset key or the brand color hex. | ## POST /projects/{id}/brand/photo **Set the brand's round photo** Replaces the round photo shown in the header of the slides. `url` must already be in Posttar's public bucket. Applies to what is generated from now on. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `url` | body | string | yes | Must be in our public bucket. | ## POST /projects/{id}/brand/logo **Set the brand's logo** Replaces the logo stamped on the slides' artwork. `url` must already be in Posttar's public bucket. Applies to every palette of the project at once. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `url` | body | string | yes | Must be in our public bucket. | --- # Radar 2 endpoints for radar, with parameters and a real response example. ## GET /projects/{id}/radar/analysis **Get the project's latest Radar analysis** Returns the project's latest Radar run. Check `estado` first: a run may be in progress, interrupted, failed, or there may be none yet. Starting a new run only happens from the Radar screen, not through the API. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | Response: ```json { "state": "completed", "startedAt": "2026-08-22T00:45:37.177413+00:00", "phase": null, "error": null, "lastCompleted": { "id": "12831dc4-f950-4125-aa85-0a31cac0c66d", "completedAt": "2026-08-22T00:45:37.177413+00:00", "insight": null, "targets": { "terms": [ "instagram" ], "hashtags": [], "yt_terms": [], "competitors": [ "use.posttar" ], "yt_channels": [] }, "kpis": { "all": { "views": 1280681274, "studied": 109 }, "youtube": { "views": 0, "studied": 0 }, "instagram": { "views": 1280681274, "studied": 109 } } } } ``` ## GET /projects/{id}/radar/inspirations **List the project's Radar inspirations** Lists the posts Radar has already found for this project, best to worst, with who published each one, the link, the caption, and the views. Read-only: does not start an analysis and does not create a post. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `platform` | query | "instagram" | "youtube" | no | | | `limit` | query | integer | no | Default 10, max 100. | | `cursor` | query | string | no | | Response: ```json { "total": 4, "next": "eyJjaGF2ZSI6OTIuOSwiaWQiOiJEYVpXZ2NYeDJMVCJ9", "inspirations": [ { "shortcode": "DcNflthvNwy", "url": "https://www.instagram.com/p/DcNflthvNwy/", "handle": "simran.verma125", "caption": "Relatable 💯…\n•\n•\n#Corporate #instagram #reels #viral #follow , Explore, Office, work, Comedy, Corporatelife, employees, reality", "platform": "instagram", "format": "video", "score": 93.4, "views": 13856429, "firstSeenAt": "2026-08-22T00:43:51.906847+00:00", "publishedAt": "2026-08-19T07:04:52+00:00" }, { "shortcode": "DaZWgcXx2LT", "url": "https://www.instagram.com/p/DaZWgcXx2LT/", "handle": "fyndingplaces", "caption": "😂🔥 Think you know every Instagram trick? Think again! 👀📱\n\nI just shared 3 fun Instagram pranks that will leave your friends totally confused! 🤯🤣 These are easy to try, harmless,...", "platform": "instagram", "format": "video", "score": 92.9, "views": 11870423, "firstSeenAt": "2026-08-22T00:43:21.423496+00:00", "publishedAt": "2026-07-05T03:32:33+00:00" } ] } ``` --- # Reports 2 endpoints for reports, with parameters and a real response example. ## GET /projects/{id}/reports/summary **Get a period summary** How many posts the project has published, scheduled, and in draft, optionally within a period. Also reports how many published posts were never measured, which is absence of measurement, not zero views. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `from` | query | string | no | | | `to` | query | string | no | | Response: ```json { "period": { "from": null, "to": null }, "published": 0, "scheduled": 0, "drafts": 21, "publishedWithoutMetrics": 0, "publishedWithoutDate": 0 } ``` ## GET /projects/{id}/reports/performance **Get posts' performance** Published posts with each one's numbers, in two lists that are never mixed: measured ones with real views, likes, and comments, and unmeasured ones Posttar never measured (their views are `null`, which is not the same as zero). | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | Opaque id from a previous response. | | `from` | query | string | no | | | `to` | query | string | no | | | `limit` | query | integer | no | Default 20, max 100. | Response: ```json { "period": { "from": null, "to": null }, "measured": [], "totalMeasured": 0, "withoutMetrics": [], "totalWithoutMetrics": 0, "totals": null } ``` --- # Team 1 endpoint for team, with parameters and a real response example. ## GET /team **List the workspace's team** Who is in the workspace, with role and email, plus pending invites, how many people the plan allows, and how many seats remain. Read-only: inviting and canceling invites are not available through the API. See this document's `x-posttar-excluded` for why. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Default 50, max 100. | Response: ```json { "yourRole": "owner", "members": [ { "id": "f8090f41-2e10-4a7d-b0b1-27051e8f2c24", "userId": "fe7840c7-4f22-42cb-8786-4e486b5db8f2", "role": "owner", "name": "Alex Rivera", "email": "member@example.com", "joinedAt": "2026-04-05T02:45:37.858819+00:00", "isYou": true } ], "pendingInvites": [], "planLimit": 1, "seatsUsed": 1, "seatsFree": 0 } ```