PowerPost APIv1
PowerPost APIv1
DashboardAPI keysPowerPost API
QuickstartAuthenticationWorkspacesErrors & Rate Limits
Input TypesPost TypesCreditsResearch Modes
Generate CaptionsGenerate ImagesPublish and ScheduleWebhooksMCP Server

Content

Generate ContentGenerations

Media

Upload MediaGenerate ImagesGenerate VideosVideo Captions

Publishing & planning

PostsPublish & SchedulePost ItemsAnalyticsCalendar

Account

Get Credits
Changelog
API reference

Video Captions

Add timed, styled captions to a video.

Add timed captions to a video in your workspace media library. The result is saved as a new MP4.

Rendering requires a prior credit purchase.

Examples

A bold sentence with one emphasized word.

Caption cues

This video uses the production renderer. Select a cue to seek to its start time.

View request JSON
Replace `media_id` with the video media ID.
{
  "media_id": "11111111-1111-4111-8111-111111111111",
  "defaults": {
    "position": {
      "anchor": "bottom",
      "x_pct": 50,
      "y_pct": 72
    },
    "style": {
      "font": {
        "family": "Montserrat",
        "size": 68,
        "weight": "black"
      },
      "text": {
        "color": "#FFFFFF",
        "align": "center",
        "line_height": 1.05,
        "case": "original",
        "max_width_pct": 82
      },
      "stroke": {
        "color": "#000000",
        "width": 5
      },
      "shadow": {
        "color": "#000000",
        "offset": 3,
        "blur": 0.8
      },
      "background": {
        "opacity": 0
      }
    },
    "animation": {
      "enter": {
        "type": "slide-up",
        "duration_ms": 140
      },
      "exit": {
        "type": "fade",
        "duration_ms": 80
      }
    }
  },
  "captions": [
    {
      "start": 0,
      "end": 1.5,
      "text": [
        {
          "text": "Use the "
        },
        {
          "text": "NORTH",
          "style": {
            "text": {
              "color": "#FFE66D"
            },
            "font": {
              "size": 82
            }
          }
        },
        {
          "text": " entrance"
        }
      ]
    },
    {
      "start": 1.5,
      "end": 3.2,
      "text": [
        {
          "text": "The market opens at "
        },
        {
          "text": "EIGHT",
          "style": {
            "text": {
              "color": "#7DD3FC"
            },
            "font": {
              "size": 82
            }
          }
        }
      ]
    },
    {
      "start": 3.2,
      "end": 5,
      "text": [
        {
          "text": "Coffee is on the "
        },
        {
          "text": "SECOND",
          "style": {
            "text": {
              "color": "#F9A8D4"
            },
            "font": {
              "size": 82
            }
          }
        },
        {
          "text": " floor"
        }
      ]
    }
  ]
}

Workflow

  1. Choose any video in your workspace media library. It can be uploaded or generated.
  2. Start a caption render with the video's media_id and your timed cues.
  3. Poll the returned status_url until the job is completed or failed.
  4. Use video.media_id to publish the new video, or video.url to preview it.

Required scopes: videos:generate to render and videos:read to check status.

Render captions

POST /api/v1/videos/captions/render

Styling is optional. If omitted, the built-in defaults are used.

# Start the render
curl -X POST https://powerpost.ai/api/v1/videos/captions/render \
  -H "x-api-key: pp_live_sk_YOUR_KEY" \
  -H "X-Workspace-Id: YOUR_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "media_id": "YOUR_VIDEO_MEDIA_ID",
    "captions": [
      { "start": 0, "end": 1.8, "text": "Meet at the station at eight" },
      { "start": 1.8, "end": 3.8, "text": "The north entrance is still open" }
    ]
  }'

# Poll using the caption_render_id from the response
curl https://powerpost.ai/api/v1/videos/captions/renders/CAPTION_RENDER_ID \
  -H "x-api-key: pp_live_sk_YOUR_KEY" \
  -H "X-Workspace-Id: YOUR_WORKSPACE_ID"
Start response
{
  "caption_render_id": "22222222-2222-4222-8222-222222222222",
  "status": "processing",
  "source_media_id": "11111111-1111-4111-8111-111111111111",
  "status_url": "/api/v1/videos/captions/renders/22222222-2222-4222-8222-222222222222",
  "created_at": "2026-08-21T10:00:00Z",
  "remaining_credits": 42
}

Rate limit: 30 requests per minute per API key.

Request reference

Top-level fields

FieldTypeRequiredDescription
media_iduuidYesVideo media ID from the same workspace
captionsarrayYes1–500 timed caption cues
defaultsobjectNoStyle, position, and animation inherited by every cue

defaults are deeply merged with the built-in defaults. A cue can then override individual fields without repeating the rest of the style.

Caption cues

FieldTypeRequiredLimits and behavior
startnumberYesStart time in seconds; must be ≥ 0
endnumberYesEnd time in seconds; must be after start and within the video duration
textstring or span arrayYes1–500 characters after spans are joined
layerintegerNo0–50; higher captions draw on top when cues overlap
styleobjectNoOverrides default style fields for this cue
positionobjectNoOverrides default position fields for this cue
animationobjectNoOverrides default animation fields for this cue

Times are rendered at centisecond precision. Keep start and end at least 0.01 seconds apart.

Built-in defaults

If you send only cue timing and text, PowerPost uses these values:

GroupDefaults
FontMontserrat, size 62, weight black
TextWhite, opacity 1, letter spacing 0, centered, uppercase, line height 1.08, max width 82%
StrokeBlack, width 4
ShadowBlack, offset 2, blur 0.8
BackgroundTransparent, padding 10, radius 0, no border
PositionBottom anchor at x: 50%, y: 72%
AnimationFade in 420ms, fade out 180ms
Style fields, positioning, and text spans

Style fields

FieldTypeAccepted values
style.font.familystringInter, Arial, Roboto, Montserrat, Impact, Poppins, Open Sans, Noto Sans, Bebas Neue, Archivo Black
style.font.sizeinteger10–320 video pixels. Two letters on 1080p often need 240–320.
style.font.weightstringregular, medium, bold, black
style.text.colorstringSix-digit hex color, such as #FFFFFF
style.text.opacitynumber0–1; applies to the text fill, stroke, and shadow
style.text.letter_spacingnumber-50–100 video pixels; negative values tighten text
style.text.alignstringleft, center, right
style.text.line_heightnumber0.8–2
style.text.casestringoriginal, uppercase, lowercase
style.text.max_width_pctnumber20–100 of the video width. A cap, not the box size. Shorter lines stay shorter.
style.text.underlinebooleanUnderline all cue text
style.text.strikebooleanStrike through all cue text
style.stroke.colorstringSix-digit hex color
style.stroke.widthnumber0–20 video pixels
style.shadow.colorstringSix-digit hex color
style.shadow.offsetnumber0–30 video pixels, down and right
style.shadow.blurnumber0–10; 0 is a hard edge
style.background.colorstringSix-digit hex color
style.background.opacitynumber0–1
style.background.paddingnumber0–60 video pixels around the laid-out text
style.background.radiusnumber0–80 video pixels
style.background.border.colorstringSix-digit hex color
style.background.border.widthnumber0–30 video pixels

The cue background fits the wrapped text plus padding and border. Raise max_width_pct to keep a long line together; the plate stays as wide as that line.

Font size and spacing are measured against the actual video frame. A size of 64 therefore looks proportionally smaller on a 4K frame than on a 1080p frame.

Position

x_pct and y_pct are absolute coordinates from 0 to 100. anchor selects which point on the caption box attaches to that coordinate.

ResultPosition
Exact center{ "anchor": "center", "x_pct": 50, "y_pct": 50 }
Centered near bottom{ "anchor": "bottom", "x_pct": 50, "y_pct": 72 }
Top-left inset{ "anchor": "top-left", "x_pct": 8, "y_pct": 8 }
Bottom-right inset{ "anchor": "bottom-right", "x_pct": 92, "y_pct": 92 }

Available anchors: top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right.

For Reels, Shorts, and TikTok, start near y_pct: 72 instead of the physical bottom. This leaves room for platform controls and descriptions.

Animation

Both animation.enter and animation.exit accept:

FieldTypeAccepted values
typestringnone, fade, slide-up, slide-down, slide-left, slide-right, scale
duration_msinteger0–2000
from_scalenumberEnter only; starting scale from 0.1–2, default 0.8
to_scalenumberExit only; ending scale from 0.1–2, default 0.8

If enter plus exit is longer than the cue, both durations shrink to fit.

Slide animations travel one resolved line height.

Scale animations transform the entire caption around position.anchor. Enter animations move from from_scale to normal size (1); exit animations move from normal size to to_scale. Scaling is uniform, linear, and includes every line, text span, stroke, shadow, background, and border.

{
  "animation": {
    "enter": { "type": "scale", "from_scale": 0.6, "duration_ms": 220 },
    "exit": { "type": "scale", "to_scale": 0.8, "duration_ms": 180 }
  }
}

Highlight individual words

Instead of a string, set text to an array of spans. Each span inherits the cue's text styling and can override font, color, opacity, letter spacing, underline, strike-through, stroke, or shadow. A span can also draw its own background.

{
  "start": 0,
  "end": 2.2,
  "text": [
    { "text": "Open the " },
    {
      "text": "SETTINGS",
      "style": {
        "font": { "size": 84 },
        "text": { "color": "#111111" },
        "background": { "color": "#FFE66D", "opacity": 1, "padding": 8, "radius": 4 }
      }
    }
  ]
}

Span styles accept font, text.color, text.opacity, text.letter_spacing, text.underline, text.strike, stroke, shadow, and background. A span background accepts the same color, opacity, padding, radius, and border fields as a cue background. It is independent of the cue background and does not change text spacing or wrapping. The box is the visible glyphs plus padding, border, and stroke, so an outline stays inside the chip. Leading and trailing whitespace still advances the text but is excluded from the visible background. If the span wraps, each line receives its own box.

A span's text opacity overrides the cue opacity rather than multiplying it. Letter spacing is measured in video pixels and applies after each character, including the final character's positioning advance and styled-span boundaries. Keep position, animation, line height, case, and max width on the cue or in defaults.

Get render status

GET /api/v1/videos/captions/renders/{id}

Scope: videos:read

Poll every 2–5 seconds until status is completed or failed. Rate limit: 120 requests per minute per API key.

Processing response
{
  "caption_render_id": "22222222-2222-4222-8222-222222222222",
  "status": "processing",
  "source_media_id": "11111111-1111-4111-8111-111111111111",
  "output_media_id": null,
  "error": null,
  "created_at": "2026-08-21T10:00:00Z",
  "updated_at": "2026-08-21T10:00:00Z",
  "video": null
}
Completed response
{
  "caption_render_id": "22222222-2222-4222-8222-222222222222",
  "status": "completed",
  "source_media_id": "11111111-1111-4111-8111-111111111111",
  "output_media_id": "33333333-3333-4333-8333-333333333333",
  "error": null,
  "created_at": "2026-08-21T10:00:00Z",
  "updated_at": "2026-08-21T10:02:00Z",
  "video": {
    "media_id": "33333333-3333-4333-8333-333333333333",
    "url": "https://signed-storage-url.example/output.mp4",
    "thumbnail_url": "https://signed-storage-url.example/output-thumb.jpg",
    "file_name": "source-captions-2026-08-21T10-02-00Z.mp4",
    "file_size": 12345,
    "mime_type": "video/mp4",
    "width": 1080,
    "height": 1920,
    "duration": 5,
    "created_at": "2026-08-21T10:02:00Z"
  }
}

The returned url is signed and temporary. Store the durable video.media_id and request a fresh media URL when needed. source_media_id can be null if the original video was deleted after the job was created.

Failed response
{
  "caption_render_id": "22222222-2222-4222-8222-222222222222",
  "status": "failed",
  "source_media_id": "11111111-1111-4111-8111-111111111111",
  "output_media_id": null,
  "error": {
    "code": "VIDEO_CAPTION_RENDER_FAILED",
    "message": "Caption rendering failed"
  },
  "created_at": "2026-08-21T10:00:00Z",
  "updated_at": "2026-08-21T10:02:00Z",
  "video": null
}

Common problems

ProblemWhat to check
Caption is hidden by social UIMove a bottom caption toward y_pct: 68–74
Text wraps too earlyIncrease max_width_pct, reduce font size, or shorten the cue
Busy footage hurts readabilityAdd a background or increase stroke width
A cue is rejectedConfirm end > start, the cue is at least 0.01s, and end does not exceed the video duration
A highlighted word loses its styleUse a span array and keep the spaces inside adjacent span text
Polling always returns 404Use the same workspace that created the render

When the stored source duration is available, a cue beyond the video duration returns 400. If bad media metadata prevents early validation, the background render can fail instead.

HTTP errors

StatusMeaning
400Invalid body, missing source video, non-video media, unavailable storage path, or cue beyond the known duration
401Invalid API key
402Prior purchase required or insufficient credits
403API key is missing the required scope
404Render ID does not exist in this workspace
429Rate limit exceeded

MCP tools

These tools take the same JSON as the REST endpoints above, including defaults, per-cue style/position/animation, span arrays, and scale animations.

ToolWhat it does
powerpost_render_video_captionsStarts the render and returns caption_render_id
powerpost_get_video_caption_renderPoll until completed or failed; on success, use video.media_id

Next steps

  • Upload media
  • Generate a video
  • Create a post with the captioned video
  • Publish or schedule

Generate Videos

Create AI-generated videos from text prompts or source images.

Posts

Create draft posts and fetch their status.

On this page

ExamplesWorkflowRender captionsRequest referenceTop-level fieldsCaption cuesBuilt-in defaultsStyle fieldsPositionAnimationHighlight individual wordsGet render statusCommon problemsHTTP errorsMCP toolsNext steps