Posttar API

Posts

32 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.

ParameterInTypeDescription
statusquerystringComma-separated list of statuses to filter by. The statuses: idea, queued, generating, draft, scheduled, publishing, published, failed, archived, rejected. An unknown status is refused with invalid_input, and never answered with an empty list.
fromquerystringISO date. Filters by scheduled date.
toquerystringISO date. Filters by scheduled date.
projectIdquerystringOne project, when the workspace has more than one. From GET /projects.
limitqueryintegerDefault 25, max 100.
cursorquerystringmax 2048 charsOpaque cursor from a previous response's next field. Do not build one: a malformed cursor is refused with invalid_input, never answered with page one.

Response

{
  "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",
      "format": "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...",

POST/posts202

Create a post

Creates a post from a theme and starts generating it. The format parameter picks which format to make, and defaults to carousel. Every format costs 1 credit except motion, which costs 5 credits, and a workspace makes at most 3 of those every 24 hours, 2 generating at the same time. A balance below the format's price is refused with 409 (insufficient_credits, with the price and the balance), and nothing is charged. A carousel or a read-the-caption reel is usually ready in under a minute; the narrated formats take up to about two. Fire-and-forget: responds 202 with state: "generating"; poll GET /posts/{id} for the outcome. The formats in preview are refused with 403 unless the workspace has them enabled, and nothing is charged. twitter_repost and react_video are made from the link of an Instagram post or reel instead of a theme; a video file cannot be sent through the API. A link that cannot be used (private, not a video, longer than 90 seconds) is refused before anything is charged. A react_video shows the source video in full screen with one of the person's own recorded reactions looping in the corner, and one of their recorded endings (call to action) at the end; the reactions and endings are recorded in the app, under Strategy, Avatar for Reels. A project with no reaction ready is refused with 409 (react_clips_missing) before anything is charged. The theme is an optional instruction for the caption. A motion is an animated reel of about 30 seconds made over a narration: send the idea (or theme, and optionally a voiceId) OR the audioUrl of your own narration, never both. It takes about 15 minutes; poll GET /posts/{id} and read content.stage. An animated_audio can also be made from your own narration: send its audioUrl and no theme.

Answers 202. The work finishes in the background; poll GET /posts/:id for the outcome.

ParameterInTypeDescription
projectIdrequiredbodystringThe project the post belongs to. From GET /projects.
themebodystringmax 10000 charsWhat the post is about, in the client's words. Up to 10000 characters, except on animated_audio, narrated_photos and motion (5000); twitter_repost and react_video (1000). On the narrated formats the theme becomes a spoken script; on twitter_repost and react_video it is only a note on what to highlight.
formatbody"carousel" | "caption_reel" | "animated_audio" | "narrated_photos" | "premium_carousel" | "infographic" | "twitter_carousel" | "twitter_repost" | "react_video" | "motion"Which format to generate. Defaults to carousel, the Templates Carousel. The new formats (animated_audio, narrated_photos, premium_carousel, infographic, twitter_carousel, twitter_repost, react_video, motion) are refused with 403 unless the workspace has the new formats enabled, and nothing is charged. With them enabled, carousel is a legacy format: still created on request, but kept out of the autopilot unless allowed explicitly.
ideabodystringmax 5000 charsmotion only: the idea the Posttar turns into a narrated script, up to 5000 characters. theme is accepted in its place. Send an idea OR audioUrl, never both.
voiceIdbodystringmax 100 charsmotion from an idea only: the voice that narrates, one of the Posttar voices. Leave it out to use the project's voice, the same one its narrated reels use.
audioUrlbodystringmotion and animated_audio only: a public https address of your own narration audio (mp3, m4a, ogg, opus, wav, aac, webm, or an mp4 with sound), up to 25 MB. The server downloads it once. Silences at the start, at the end and long pauses are cut; after the cuts it has to fit in 30 seconds, sped up at most 1.2x, or the post fails (on motion with the reason in content.error; on animated_audio the credit comes back). On animated_audio, send it without a theme: the audio replaces the script and the voice, and the caption is written from what was said.
linkbodystringA URL to read before writing. Only three formats take a link. carousel reads it and theme becomes the instruction for what to do with it. twitter_repost takes the link of an Instagram post or reel (…/p/…, /reel/…, /reels/…) whose video is reposted, up to 90 seconds; theme is then an optional note on what to highlight. react_video takes the same kind of link, for the video the person reacts to; theme is then an optional note on what to comment. Video files are not accepted through the API. Any other format is refused, because ignoring the link would spend a credit on a post about the wrong subject.

Response

{
  "postId": "dd8d142c-4cba-4ea6-9088-e948ac347f56",
  "format": "premium_carousel",
  "state": "generating",
  "remainingCredits": 37
}

GET/posts/{id}

Get a post

Returns one post in full, in any format. The envelope is always the same, format says which format it is, and content carries what belongs to that format: slides for the four carousel-shaped formats, the text blocks and look for caption_reel, the script and scenes for the two narrated ones (on animated_audio, origin is audio when it was made from your own narration, and then the script is its transcript and cannot be changed), and for twitter_repost the hook, the header author (name, handle, photoUrl), where the video came from (sourceKind, sourceUrl, sourceHandle), its durationSeconds and the videoUrl of the rendered MP4 (the finished reel, the same file as mediaUrls; the source video is not exposed). For react_video it carries the mode (recorded, made with the person's recorded reaction; ai, an older React with an AI face and speech, whose speech is kept read-only), the reactionClipId and ctaClipId in use, the same source fields and the videoUrl of the rendered MP4. For video_clip, content.clip has the hook, the title, the stretch in use (startSec, endSec), the proposed one it can be trimmed around (proposalStartSec, proposalEndSec), the jobId it came from, its renderState and finalized (true 30 days after it was published: the stretch can no longer be trimmed nor the video remade, and the finished video in mediaUrls is what downloads and publishes). For motion it carries the origin (idea or audio), the idea, the narrated script, the stage (preparing, directing, animating, rendering, ready, altering or failed, with the whole reason in error), whether its one change was used (alterationUsed, alterationRequest), canUndo (true when there are two versions to switch between), activeVersion (original or altered, which one is in the video now; null before a finished change) and the videoUrl. content is null while the post is still generating, which is a real state and not an error: read status to know.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "id": "d9af393d-efcf-4083-b446-15a158eac89c",
  "status": "draft",
  "format": "carousel",
  "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359",
  "workspaceId": "2bd5c04d-1dd4-456e-8704-9d7b84703fd0",
  "scheduledFor": null,
  "textContent": "Você não precisa decorar todas as métricas do Instagram. Precisa saber o que cada uma está tentando te contar.\n\nAlcance mostra ...",
  "mediaOutOfDate": false,
  "mediaUrls": [
    {
      "url": "https://...-15a158eac89c_slide_1_render_1789411861179.png",
      "type": "image"
    },
    {
      "url": "https://...-15a158eac89c_slide_2_render_1789411860205.png",

PATCH/posts/{id}

Update a post

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
captionbodystringmax 2200 charsThe Instagram caption, replaced in full. Up to 2200 characters, the same ceiling Instagram has.
reelbodyobjectOnly on the caption_reel format: the three text blocks and the look of the video.
.block1bodystringThe hook, at the start of the video.
.block2bodystringThe complement, at 2s. May be empty.
.block3bodystringThe call to the caption, at 5s.
.fontbodystring
.alignmentbody"center" | "left"As returned in content.alignment by GET /posts/{id}.
.boxStylebody"rounded" | "square"As returned in content.boxStyle by GET /posts/{id}.
.boxColorbodystringHex, applies to all three blocks.
.textColorbodystringHex, applies to all three blocks.
.boxColorByBlockbody(string | null)[]Per-block box color. Null reverts to the general color.
.textColorByBlockbody(string | null)[]Per-block text color. Null reverts to the general color.
.sizesbody(number | null)[]Per-block font size, 20 to 140.
narrationbodyobjectOnly on animated_audio and narrated_photos: the spoken script and what is drawn over it. Changing script does NOT re-record the audio.
.hookbodystringThe line at the top of the video, up to 120 characters.
.scriptbodystringThe full narrated text, with emotion markup. Changing 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. Re-recording is not available through the API.
.speedbodynumberBetween 1 and 1.5.
.scenesbodyobject[]max 60 itemsanimated_audio only. The complete scene list, replacing the current one; an empty list removes every illustration. Read the current ones in content.scenes from GET /posts/{id}. Each scene carries only the fields of its own type; the other field names are accepted as null so a scene read from that call can be sent back untouched, and a field that belongs to another type is refused with its name.
.scenes[].type: "keyword"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "big_number"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "before_after"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "bullet_list"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "highlight"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "numbered_steps"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "question"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "ratio_bar"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "bar_chart"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "rising_line"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "progress_ring"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "timeline"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.scenes[].type: "wave_only"bodyfields: { value, label, text, before, after, items, steps, percent, bars }
.photoScenesbodyobject[]max 60 itemsnarrated_photos only. The complete scene list, as returned in content.scenes by GET /posts/{id}.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c"
}

DELETE/posts/{id}

Delete a post

Permanently deletes the post. Irreversible.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c"
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
mediarequiredbodyobject[]max 10 itemsThe full media list, at most 10 items (the Instagram carousel limit). An empty list removes the media.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c"
}

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. An own post can be scheduled while its media is still preparing: it publishes once the media is ready, and moves to failed if the preparation fails. An own post whose media failed to prepare is refused with 409 (own_media_failed): send the media again first. A project with no Instagram account at all is refused with 409 and a message starting with instagram_not_connected: connect the account in the Posttar app first. An account that is only temporarily disconnected is accepted: the post waits for it to come back.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
whenrequiredbodystringISO 8601 with a timezone, in the future. Either Z or an offset like -03:00.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c",
  "scheduledFor": "2026-09-20T14:13:29.128Z"
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c"
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c",
  "stillOnInstagram": false
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c"
}

POST/posts/{id}/publish202

Publish a post now

Publishes the post to Instagram right away. Fire-and-forget: responds 202 with the post in publishing state, usually done in under a minute; poll GET /posts/{id} for the outcome. An own post whose media is still preparing responds 202 with state: "waiting_for_media": it is scheduled for now and publishes as soon as the media is ready. One whose media failed to prepare is refused with 409 (own_media_failed). A project without a connected Instagram account is refused with 409 and a message starting with instagram_not_connected, before anything is queued: connect the account in the Posttar app first. Irreversible.

Answers 202. The work finishes in the background; poll GET /posts/:id for the outcome.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c",
  "state": "publishing"
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
layoutIdbodystringA layout id from GET /slides/{id}/layouts, taken from any slide of this post. Without it, inherits the layout of the post's last slide.

Response

{
  "slideId": "b4dfcd93-31b8-4a9e-a7ed-e6fb38781c29",
  "number": 9,
  "totalSlides": 9,
  "media": "untouched"
}

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
orderrequiredbodystring[]max 50 itemsThe complete list of slide ids, in the new order.

Response

{
  "postId": "d9af393d-efcf-4083-b446-15a158eac89c",
  "order": [
    "d32ab31a-073e-471f-8d7a-d145c587e653",
    "f2e8d7f9-73d4-4fc3-b674-3fd6c908d55f",
    "717eadc6-2918-475d-8098-2cefc9bb2d92"
  ],
  "media": "synced"
}

GET/posts/{id}/edit-budget

Get the 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 the budget before spending one instead of risking a call just to find it empty. The cap comes from the plan and from the format, and this endpoint is what reports it: do not hardcode a number.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

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

POST/posts/{id}/regenerate

Generate a failed post again

Generates again a post whose generation failed, in any format, on the same post and with the same theme. No credit is charged, and there is no limit on attempts: the failure was not yours. The exception is motion: 1 free attempt; if that one fails too, the post stays failed, its credits come back, and generating it again is refused with 409. Only a post with status failed and no content yet qualifies. A post that failed to PUBLISH already has its content and answers 409: publish it again instead. Fire-and-forget: responds with status: "generating"; poll GET /posts/{id} for the outcome.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "a31b83d3-3736-4578-b49e-62148f9e8b36",
  "status": "generating"
}

POST/posts/own

Create an own post

Creates a draft for a post with your own photos or videos: you bring the media, Posttar crops it to the Instagram format and publishes it. Costs no credit, and needs an active subscription (refused with 403 and a message starting with subscription_required otherwise). Optionally planned for a date (plannedFor) with a note; a planned post without media is not published. Next, send the media with POST /posts/{id}/own-media.

ParameterInTypeDescription
projectIdrequiredbodystringThe project the post belongs to. From GET /projects.
placementrequiredbody"feed" | "reel"Where it goes on Instagram: feed (photos or videos, 4:5) or reel (one video, 9:16). Defaults to feed; the media call can change it.
plannedForbodystring | nullOptional planned date (ISO 8601 with timezone). A planned post without media is not published.
planNotebodystring | nullA note for yourself about the plan, up to 500 characters. Never published.
captionbodystring | nullThe Instagram caption, up to 2200 characters and 5 hashtags.

Response

{
  "postId": "8a1f0000-0000-4000-8000-000000000000",
  "status": "draft"
}

POST/posts/own/upload-url

Get an upload address for an own post

Returns a signed address (signedUrl) to upload one photo or video, up to 100 MB, straight from your side, and the path to send as uploadPath in POST /posts/{id}/own-media. Upload the file with a PUT to signedUrl. Through MCP, send the file as an https url instead: an assistant cannot upload bytes.

ParameterInTypeDescription
projectIdrequiredbodystringThe project the file is for. From GET /projects.
mimeTyperequiredbody"image/jpeg" | "image/png" | "image/webp" | "image/heic" | "image/heif" | "video/mp4" | "video/quicktime"The file type. The real type is read from the bytes afterwards.
sizerequiredbodyintegermax 104857600The file size in bytes, up to 104857600 (100 MB).

Response

{
  "path": "2bd5c04d-0000-4000-8000-000000000000/incoming/5b7e0000-0000-4000-8000-000000000000.mp4",
  "signedUrl": "https://.../upload/sign/...?token=...",
  "token": "eyJhbGciOi..."
}

POST/posts/{id}/own-media202

Set the media of an own post

Sets the photos or videos of an own post, replacing the current ones. Feed: 1 to 10 items, cropped to 4:5 (1080x1350). Reel: one video, cropped to 9:16 (1080x1920), with an optional cover. Videos from 3 s to 15 min, files up to 100 MB. The crop fills the frame around crop. Fire-and-forget: responds 202 with state: "preparing"; poll GET /posts/{id} until content.ownMediaStatus is ready or failed. You can schedule right away with POST /posts/{id}/schedule, even while preparing: the post only publishes once the media is ready, and if preparation fails a scheduled post moves to failed. On failed, content.ownMediaError says why: video_too_short, video_too_long, video_duration_unknown, download_failed, unsupported_media, too_many_items, reel_needs_one_video, cover_only_on_reel, cover_frame_out_of_range, carousel_video_too_long (a video inside a carousel can be at most 60 s; publish it as a reel instead), subscription_required, media_in_use (the file already belongs to another post; upload it again), timed_out (not done in 15 minutes) or processing_failed. Needs an active subscription.

Answers 202. The work finishes in the background; poll GET /posts/:id for the outcome.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
placementrequiredbody"feed" | "reel"feed (cropped to 4:5) or reel (cropped to 9:16).
mediarequiredbodyobject[]max 10 itemsFeed: 1 to 10 photos or videos, cropped to 4:5. Reel: exactly one video, cropped to 9:16. Each item is an https URL we download, or an uploadPath from /posts/own/upload-url; send exactly one of the two. An empty list removes the media.
coverrequiredbodyobject | nullReel only. A frame of the video (offsetMs from the start) or a photo (url or uploadPath). Null uses the default cover.

Response

{
  "postId": "8a1f0000-0000-4000-8000-000000000000",
  "state": "preparing"
}

POST/posts/{id}/premium-signature

Show or hide the brand signature on a Premium Carousel

Turns the brand signature (profile photo, name and @handle, in the bottom-left corner of every slide) on or off for a premium_carousel post, and for the project's next ones. It is free and calls no model: the slides are rebuilt from their clean images at once. signed is false when the signature is on but the profile has no name and no @handle to write. The signature is on by default.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
enabledrequiredbodybooleantrue shows the brand signature (profile photo, name and @handle) on every slide; false removes it.

Response

{
  "postId": "9c4e2a71-5b3d-4f8a-a6e0-7d1c3b9f2e58",
  "enabled": true,
  "signed": true,
  "slides": [
    {
      "slideId": "3f6b1c2d-8e4a-4b7f-9c1d-2a5e7f9b0c13",
      "url": "https://.../post-images/composed/9c4e2a71/1.webp"
    },
    {
      "slideId": "7a2d9e4f-1b3c-4d5e-8f6a-0b1c2d3e4f57",
      "url": "https://.../post-images/composed/9c4e2a71/2.webp"
    }
  ]
}

POST/posts/{id}/tweets

Add a tweet slide

Inserts a new slide into a twitter_carousel post, right after afterSlideId, or at the end when it is omitted. Free.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
textrequiredbodystringmax 280 charsThe new slide's tweet text, up to 280 characters.
afterSlideIdbodystringA slide id of this post, from GET /posts/{id}. The new slide is inserted right after it. Without it, the new slide goes at the end.

Response

{
  "slideId": "d3a1f2c4-7e88-4b1a-9c3f-2a0b6d4e5f11",
  "url": "https://.../post-images/twitter-carousel/d3a1f2c4/9b6c1e02.png",
  "order": 3
}

PATCH/posts/{id}/tweet-author

Set the Twitter Carousel author

Changes the name, handle and photo shown on every slide's header of a twitter_carousel post, and redraws the whole carousel with the new author. Send only what changes; an omitted field keeps its current value, and photoUrl: null removes the photo. Slower than editing one slide, since every slide in the post is redrawn.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
namebodystringmax 100 charsThe display name on every slide's header, up to 100 characters.
handlebodystringmax 60 charsThe handle on every slide's header, up to 60 characters, sent exactly as it should render (include the leading '@' if you want one shown, for example '@alexrivera').
photoUrlbodystring | nullA Posttar-hosted image address for the header photo. Null removes it.

Response

{
  "slides": [
    {
      "slideId": "b4dfcd93-31b8-4a9e-a7ed-e6fb38781c29",
      "url": "https://.../post-images/twitter-carousel/b4dfcd93/1a2b3c4d.png"
    },
    {
      "slideId": "d3a1f2c4-7e88-4b1a-9c3f-2a0b6d4e5f11",
      "url": "https://.../post-images/twitter-carousel/d3a1f2c4/5e6f7a8b.png"
    }
  ]
}

PATCH/posts/{id}/repost

Edit a Twitter-style Repost

Changes the hook above the video and who appears in the header of a twitter_repost post. It is free and does not render: the post is marked as having an outdated video, and the MP4 is rebuilt before it is downloaded, scheduled or published. The video itself cannot be changed. The caption is edited with PATCH /posts/{id}, as in every format.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
hookbodystringmax 480 charsThe line above the video, up to 120 visible characters. Wrap words in double asterisks for bold.
authorbodyobjectWho appears in the header of the repost.
.namebodystringmax 100 charsDisplay name in the header, up to 100 characters.
.handlebodystringmax 60 charsHandle in the header, sent exactly as it should render, up to 60 characters.
.photoUrlbodystring | nullA Posttar-hosted image for the header photo. Null removes it.

Response

{
  "postId": "b31de50c-580c-40eb-a092-5b763333bc03",
  "hook": "O **GLP-1** pode ajudar contra vícios, diz este médico",
  "mediaStale": true,
  "author": {
    "name": "Alex Rivera",
    "handle": "@alexrivera",
    "photoUrl": "https://.../post-images/posts/c85848ba/ee1e8a72_slide_1_render.png"
  }
}

PATCH/posts/{id}/react

Swap a React's reaction or ending

Changes which of the project's recorded reactions loops in the corner of a react_video post, or which ending (call to action) closes it; ctaClipId: null removes the ending. Only ready videos of the same project are accepted. It is free and does not render: the post is marked as having an outdated video, and the MP4 is rebuilt before it is downloaded, scheduled or published. Older Reacts (mode: "ai") cannot swap. The caption is edited with PATCH /posts/{id}, as in every format.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
reactionClipIdbodystringmax 64 charsThe reaction video to use, from the project's ready reactions.
ctaClipIdbodystring | nullThe ending (call to action) video to use, from the project's ready endings. null removes the ending.

Response

{
  "postId": "5f0b7c2e-9a41-4d8e-b3c6-2e7d1a9f4b10",
  "reactionClipId": "6f1c2a9e-3b7d-4e21-9a55-0c8d4f2b7e10",
  "ctaClipId": "a83d5e10-7c2b-4f6a-8e91-3d0b6c4f2a77",
  "mediaStale": true
}

POST/posts/{id}/motion/alteration202

Change a Motion

Asks for the ONE change a motion post has, in plain words: the animation is rewritten from the current one, keeping the narration. It is free, and only works when content.stage is ready and the change was not used yet. Fire-and-forget: responds 202 with stage: "altering"; poll GET /posts/{id} until content.stage is ready again. If the change fails, the Motion goes back to how it was, the change is given back and content.error says why. After it, POST /posts/{id}/motion/undo switches between the original and the changed version.

Answers 202. The work finishes in the background; poll GET /posts/:id for the outcome.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
requestrequiredbodystringmax 500 charsWhat to change in the Motion, in the client's words, 3 to 500 characters. Every Motion has ONE change.

Response

{
  "postId": "9c4e2a71-5b3d-4f8a-a6e0-7d1c3b9f2e58",
  "stage": "altering"
}

POST/posts/{id}/motion/undo

Switch a Motion between its original and changed version

Switches a motion post between the version from before its change and the changed one, video and thumbnail included. Nothing is discarded: calling it again switches back, as many times as needed. Responds with activeVersion (original or altered), the one now in the video, which is also what publishing and scheduling use. The change stays used: switching does not give it back. Only works when content.canUndo is true. The path keeps its old name, undo, so existing calls keep working.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "postId": "9c4e2a71-5b3d-4f8a-a6e0-7d1c3b9f2e58",
  "videoUrl": "https://.../renders/m0t10nv1d0/out.mp4",
  "alterationUsed": true,
  "activeVersion": "original"
}

GET/clip-jobs/{id}

Get a Video Clips job

Reads a Video Clips job: one long video cut into short vertical clips. These are the job's proposed moments and the clips made from them, not the reel's library clips of GET /posts/{id}/clips. status goes analyzing (a few minutes, nothing charged), then ready with the proposals, each with its index, start, end and hook. After approval it is rendering and then done, and clips lists each clip with its postId, renderState and videoUrl. A video that cannot be used ends failed with refusal, and nothing is charged. The job is started from the app or in a conversation; the original video is not exposed.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "id": "5f0c7a2e-3b1d-4c8e-9a6f-2d4b8e1c7a90",
  "projectId": "c85848ba-2881-4a2e-9bdd-841b26d07359",
  "status": "rendering",
  "durationSec": 1520.4,
  "refusal": null,
  "error": null,
  "proposals": [
    {
      "index": 0,
      "start": 95.2,
      "end": 138.6,
      "hook": "Why most freelancers **undercharge**",
      "title": "Pricing",
      "score": 88,
      "approved": true

POST/clip-jobs/{id}/approve

Approve Video Clips

Turns the chosen proposals of a job into clips, each one a post of format video_clip. It costs 1 credit per new clip, charged all at once: approving 3 clips uses 3 credits, and a balance short of that is refused with invalid_state before anything is charged. clips takes proposal indexes from GET /clip-jobs/{id}, not the reel's library clips of GET /posts/{id}/clips. A job can be approved again later, for proposals not approved yet, while its original video is kept (sourceAvailable). A proposal that was already approved is not charged again: it comes back in alreadyApproved, and repeating a call returns the same postIds with credits 0. postIds lines up by position with clips; an entry is null when that proposal was approved before and its post was deleted since. A hook sent for a proposal that was already approved is not applied: its index comes back in ignoredHooks (a clip's hook is changed with PATCH /posts/{id}/video-clip). credits is null only when the approval went through but the answer was lost, so this call cannot tell how much it charged; read the job. The clips render in the background; read the job, or each post, until they are ready. A clip that fails can be made again with POST /posts/{id}/regenerate.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
clipsrequiredbodyinteger[]max 8 itemsThe index of each proposal to turn into a clip, from 1 to 8 of them, from proposals in GET /clip-jobs/{id}. Each new one becomes a post and costs 1 credit, charged all at once; one already approved is not charged again. These are proposals of this job, not the reel's library clips of GET /posts/{id}/clips.
hooksbodyobjectA new hook for some of the chosen clips, keyed by index, each up to 60 characters. A chosen clip without an entry keeps the proposed hook.

Response

{
  "jobId": "5f0c7a2e-3b1d-4c8e-9a6f-2d4b8e1c7a90",
  "postIds": [
    "0e4d9b1a-6c2f-4f7e-8b3a-5d1c9e2f4a61",
    "7a3b5c9d-1e2f-4a6b-9c8d-3e5f7a9b1c24"
  ],
  "credits": 2,
  "alreadyApproved": [],
  "ignoredHooks": []
}

PATCH/posts/{id}/video-clip

Edit a Video Clip

Changes the hook of a video_clip post and trims its stretch a few seconds either way. It is free and does not render: the post is marked as having an outdated video, and the MP4 is rebuilt before it is downloaded, scheduled or published. Send only what changes. The caption is edited with PATCH /posts/{id}, as in every format.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
hookbodystringmax 240 charsThe line on top of the clip, up to 60 visible characters. Wrap words in double asterisks for bold. No links and no @handles.
startSecbodynumberWhere the clip starts in the original video, in seconds. It can move up to 5 seconds either way from content.clip.proposalStartSec in GET /posts/{id}.
endSecbodynumberWhere the clip ends, in seconds, up to 5content.clip.proposalEndSec. The clip has to stay from 20 to 60 seconds long.

Response

{
  "postId": "7a3b5c9d-1e2f-4a6b-9c8d-3e5f7a9b1c24",
  "stale": true
}

GET/posts/{id}/clips

List 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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "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. This does not have a way back. A reel whose clip Posttar chose (a stock clip, or one generated by AI) moves to the library source for good: no endpoint, and no screen, returns it to the original. Pick the clip you want before calling, or create another reel.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
clipIdrequiredbodystringA clip id from GET /posts/{id}/clips. The one in use right now is content.clipId in GET /posts/{id}, under the same name. Only a clip already in this project's video library: there is no parameter that accepts a URL, and the swap has no way back.

Response

{
  "postId": "90691f61-fe5c-442e-89b9-aa0c4d397e28",
  "clipId": "8236ef01-f035-466b-965c-aa54dcf7d8fe"
}

GET/posts/{id}/tracks

List 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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.

Response

{
  "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,

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.

ParameterInTypeDescription
idrequiredpathstringmax 200 charsOpaque id from a previous response. Do not build one: an id that is not a valid identifier is refused with invalid_input, and one that belongs to another workspace with not_found.
trackIdrequiredbodystring | nullA track id from GET /posts/{id}/tracks. Null mutes the reel. The one in use right now is content.trackId in GET /posts/{id}, under the same name.
startSecondsbodynumberWhat second of the track the reel's 7 seconds start from, and it reads back as content.startSeconds. Must leave the whole 7 seconds inside the track when its duration is known. Ignored when trackId is null, because muting has no entry point.

Response

{
  "postId": "b713e349-e71b-4c58-be38-ebb48998c344",
  "trackId": "fda25fc1-6f3f-4eb6-b7d5-c53501a8c077",
  "startSeconds": 18
}