Response
200 Project { project, messages }. project.has_published_post is true once Pamba published a post for the project (completed, not a draft, not deleted), whether or not its live link is matched yet. project.editor_state is the saved timeline (null before the first save) and project.editor_state_version the integer to send back as base_version on PATCH /projects/{projectId}/editor-state or POST /projects/{projectId}/render. project.posted_tiktok_video_url and project.posted_instagram_video_url are the live posts once verification matched them, with project.posted_instagram_username the account the reel is on (a reel's link names no account; a reel deleted through Pamba before its check found it keeps the deleted reel's link, which is no longer live); POST /devices/accounts/post-deletions takes either link as a post id. project.posting_accounts is { scope: "avatar" | "workspace", tiktok_usernames, instagram_usernames, instagram_usernames_with_facebook, instagram_usernames_cross_posting_to_facebook, instagram_usernames_below_trial_floor, instagram_usernames_personal }: the accounts a schedule may name, the choices the app's schedule dialog offers. scope avatar lists the avatar's post-enabled accounts (an Instagram account only while it is on its phone); scope workspace, for a project or slideshow with no avatar, lists every postable account on the workspace's own phones, whichever avatar it is linked to (a TikTok account the workspace paused or archived is left out). A listed TikTok account that is not signed in on one of the workspace's phones, or one with an open suspension, is still refused when scheduled. instagram_usernames_with_facebook names the Instagram accounts whose linked Facebook account can post too (share_to_facebook), and instagram_usernames_cross_posting_to_facebook those of them whose Cross-post reels to Facebook switch is on: their reels go to Facebook unless a post sends share_to_facebook false, and only they take share_to_facebook true; instagram_usernames_below_trial_floor names those whose last follower count Pamba read is under the 200 Instagram asks of a trial reel (trial_reel is refused on them; an account Pamba has not read yet is not named), and instagram_usernames_personal those Pamba knows are personal accounts with no switch to creator queued (trial_reel is refused on them too; POST /devices/accounts/profile-edits with instagram_account_type creator queues the switch). Handles carry no @. The editor_state shape:
EditorState {
main_video_track: [MainTrackEntry] required: the ordered timeline of source clips
overlay_tracks?: [OverlayTrack]
markers?: [{ id, time, color?, label? }]
main_track_magnet?: boolean false lets main-track entries carry gap_before
media_pool?: [{ src, type: image | video | audio, label?, aspect?, source_duration? }] the project's uploaded library; UI only, the render never reads it
version?, legacy_overlays_materialized?, auto_color_adjust_created?, removed_source_clips?: [{ clip_id, video_url? }], storyboard? Studio bookkeeping: send back what you read, never author (storyboard is opaque JSON)
}
MainTrackEntry {
clip_id: UUID required
in_point (0), out_point? seconds into the source clip
type ("video"), speed?, volume?, color?
filter?: none | warm | cool | mono | vivid | film
transform?: { scale (required), x (0), y (0) }
crop?: { x, y, w, h (required, fractions of the rotated source), rotation (0) }
transition_after?: { type (required): none | fade | dipblack | crossfade | dipwhite | whiteflash | slideleft | slideup | spin | zoompunch | whipleft | whipup | roll | shake, duration (required, seconds) }
gap_before?, fade_in?, fade_out? seconds (gap_before needs main_track_magnet false)
use_original_voice?: boolean
voice_effect?: deep | reverb | room | phone | cheap_mic | no_bass | crisp | bass_boost | podcast | full | voice_focus (null or "original" = none), voice_strength? 0..100 (null = 100)
}
OverlayTrack { id (required), kind (required): overlay | audio, name (""), muted?, hidden?, items: [OverlayItem] }
OverlayItem, every type: id, type: text | image | video | audio | caption | adjust | motion, start, duration all required; start and duration are timeline seconds
text: text, x, y, w (fractions of the frame, 0..1: x 0.5 is centered; y is the vertical CENTER, 0 is the top edge, a top hook sits at y 0.12 to 0.2; always set x and y, null renders center-frame), style_preset (an id from GET /editor/text-style-presets; the save expands it into style, so prefer it over a hand-built style) or style: EditorTextStyle, text_wrap? (true makes w the box width the text wraps inside; otherwise break lines with \n in text), animation?: { in: none | fade | pop | slide_up | slide_down, out: none | fade | pop | slide_down | slide_up, duration (0.3) }, rotation?, opacity?
caption: words (required): [{ text, start, end }], caption_style: bold-pop | box-highlight | caps-box | karaoke | modern | modern-box | modern-word | one-word | pill | yellow-box | yellow-classic, x, y, w, style? (style.font_size sets the size), caption_look?: CaptionLook (present: the motion engine draws the caption in that look instead of the subtitle burn; captions of one voice share one: a track gives every caption the same look unless its voices are styled apart; only in a workspace whose motion_overlays is true on GET /workspaces: elsewhere a save or render carrying a caption_look or a motion item answers 400)
CaptionLook { name?, mode? (phrase | word: one word at a time), font? (a MotionStyle font), weight?, italic?, uppercase?, letter_spacing?, line_height?, color?, stroke_color?, stroke_width? (fraction of the font size), shadow? (none | soft | hard | glow), shadow_color?, fill? (behind the whole phrase), radius?, padding?, active_color? (the spoken word), active_fill? (a box behind it), active_scale? (1..1.4), spoken_color? (words already said), upcoming_opacity? (0..1; 0 reveals words as spoken), enter? (none | fade | pop | slide_up | zoom), enter_duration? }
adjust: adjust: { temperature?, tint?, saturation?, brightness?, contrast? } each -100..100 with 0 neutral; lut? ("pamba_v1" is the default grade)
image, video: src (a Pamba image or video URL; upload any other file first with POST /projects/{projectId}/upload-editor-asset and use the url it returns; a signed media-library URL expires and is refused), x, y, w (the width as a fraction of the frame: 1.0 fullscreen, about 0.6 a floating insert, about 0.4 a logo flash; the aspect ratio keeps itself), media_in? (source trim start, seconds), speed? (0.5 to 2.0; the item consumes duration x speed source seconds), volume? (0 keeps an inserted clip from talking over the presenter), fade_in?, fade_out?, opacity?, rotation?, crop? (as on MainTrackEntry), aspect?, source_duration?, label?
audio: src (a Pamba audio URL or a bundled /audio/music/... or /audio/sfx/... track; upload any other file first with POST /projects/{projectId}/upload-editor-asset), volume?, fade_in?, fade_out?, media_in?, speed?, voice_effect?, voice_strength? (as on MainTrackEntry)
motion: an animated graphic (a scoreboard, a list, a card, rain): x, y, w, aspect (its box: center and width as frame fractions, aspect = box width / height in pixels), label?, opacity?, motion: MotionSpec (required). start and duration follow the beats' anchors; an editor or API change that moves a beat re-derives them
MotionSpec {
v: 1, name (required, up to 60 chars), full_frame? (covers the frame; the box is ignored), speaker? ({x, y, scale}: a green-screen backdrop, full_frame only, which hides the footage and draws the speaker cut out of it in front, the footage frame scaled by scale (0.25-1.4) and centred at x, y; the render cuts the speaker out of each clip the backdrop spans, once per clip video), until_end? (lasts to the end of the video), cut? (every anchored beat was trimmed away: kept, not drawn)
beats: [{ key (required, unique), label, at (seconds after the overlay's start), anchor?: { clip_id, t (seconds in that clip's own time), word_index?, word? } }] up to 48; key "out" is where the overlay leaves
elements: [MotionElement] up to 24
}
MotionElement {
id (required, unique), kind (required): text | shape | image | particles | media, name, x, y, w, h (fractions of the overlay's box, top-left origin), rotation?
text?, steps?: [{ beat, text }] (the text changes at that beat) text: up to 400 chars, shrinks to fit its box
shape?: rect | pill | circle | line | arrow | triangle | star | heart | check | cross | cursor | burst
src? (a Pamba storage image; for media, a Pamba storage video or image), fit?: cover | contain, prompt? (what the picture shows), asset_id?
media: src (required), media_in? (the second of the video it starts from); plays in a card its style dresses (fill, radius, padding, border, shadow); show and hide take only none | fade | slide_up | slide_down | slide_left | slide_right | zoom, and no emphasis or loop; elements after it draw over it
particles?: { preset: rain | snow | confetti | sparkles | bubbles | hearts, density 1-400, speed, angle (degrees off vertical), color?, size? }
style?: { font?: TikTok Sans | Inter | Montserrat | Oswald | Poppins | Archivo | Bebas Neue | Anton | Instrument Serif | Playfair Display | DM Serif Display | Noto Serif | Caveat | Permanent Marker | Bangers | Roboto Mono, weight? 100-900, italic?, size? (font size as a fraction of the box width), color?, align?: left | center | right, valign?: top | middle | bottom, line_height?, letter_spacing? (em), uppercase?, stroke_color?, stroke_width? (fraction of the font size), shadow?: none | soft | hard | glow, shadow_color?, fill?, radius? (0-0.5), border_color?, border_width?, opacity?, padding? } colors are #rrggbb or rgba()
show?: { beat, effect: none | fade | pop | slide_up | slide_down | slide_left | slide_right | zoom | drop | wipe_right | wipe_down | typewriter, duration? }
hide?: { beat, effect: none | fade | pop | slide_up | slide_down | slide_left | slide_right | zoom | wipe_left, duration? }
emphasis?: [{ beat, effect: pop | shake | flash | bounce, duration? }], loop?: { effect: pulse | float | wobble | blink | spin | sway, period?, amount? }
}
EditorTextStyle {
font_family (""), font_size (64; px on the 1080x1920 frame, 24 is the readable minimum), color ("#FFFFFF"), background ("transparent")
background_radius? (0 sharp to 0.5 capsule), background_mode?: line (a box per line, what the box presets use) | block (one pill; the read when unset), background_height? (0..1, default 0.20), background_width? (0..1, default 0.28)
bold (false), italic (false), align (center): left | center | right, outline (false), shadow (false), underline?, text_case?: upper | lower | title, letter_spacing?
outline_color?, outline_width?, shadow_color?, shadow_opacity?, shadow_blur?, shadow_distance?
box_color?, box_radius?, box_pad? the caption active-word highlight box
}
Read-back only, omit on write: wrap_lines, background_box, line_boxes and words[].box are measurement caches the Studio writes. A text item's line breaks are the \n in its text: the save keeps its wrap_lines in step with them (a text_wrap box keeps the lines the Studio measured), so read the lines from text. Reserved: origin values auto_color and media_scene, asset_id, bullet_index.