Scrub API

Every Scrub tool over HTTP, the same tools as Scrub over MCP. Send a key from Account as Authorization: Bearer. POST /v1/tools/quote prices work before it runs, free. A job that takes time answers at once with its job_id and status running; ask POST /v1/tools/job_status until its status is succeeded, or failed, refused or cancelled, each answered 200 with what happened and what was charged.

Base URL https://app.scrub.app · Key from Account (get one, and try the API) · Description openapi.json

Every tool here is also a tool over MCP, with the same name, the same fields and the same price.

Work

Actions and tools: the work Scrub does, priced by the work

POST/v1/tools/add_captionspriced

Caption a video

Captions a video from the video alone, as subtitles burned into the picture, at most two lines and seven seconds each. Give a transcription to skip transcribing. Takes src (file), transcript (file, optional). Returns video, preview, edl (the captions as drawn, each with its words, times, place and style, and the source they were drawn on). One job: transcribing, then making captions, then burning in captions. Given transcript, it skips transcribing. $0.10 a minute of video + $0.15 per 10 minutes of audio, up to 60 minutes. quote gives its exact price. Afterwards, change the result with adjust on the job of the step that made it, listed in the result.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 2 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
transcript optionalstringA file_id from create_upload, accepting application/vnd.scrub.transcript+json, application/json, up to 16 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
position optionallower_third | centre | top_leftWhere the captions sit.
style optionalclean | pill | box | bold | subtitlesHow the captions look. clean: White, heavy, a few words at a time. Each word lights up as it is said. pill: The word being said sits in a white pill, in black. box: The phrase on a dark rounded box, for busy footage. bold: Heavy words that pop in as they are said; the word being said grows. subtitles: One or two full lines, smaller, without movement: for talks and wide videos.
font optionalInter Tight | Inter | Manrope | Archivo | Montserrat | Poppins | Figtree | Bricolage Grotesque | Rubik | Bebas Neue | Anton | Oswald | League Gothic | Fraunces | Playfair Display | Instrument Serif | DM Serif Display | PT Serif | Caveat | Bangers | Luckiest Guy | JetBrains MonoThe font the captions are set in. Inter Tight: Tight and even; every caption style's face. Inter: The plainest, most legible sans; explainers and data. Manrope: Calm, geometric and modern. Archivo: Sturdy grotesque with real weight; news and how-tos. Montserrat: The face most short-form captions are set in. Poppins: Round geometric sans; friendly brands and lifestyle. Figtree: Clean and warm, a little softer than Inter. Bricolage Grotesque: Quirky editorial sans. Rubik: Slightly rounded corners; playful but tidy. Bebas Neue: Tall capitals; titles, numbers and sport. Anton: Heavy, narrow capitals; the loudest hook text. Oswald: Condensed, with weights; lists and labels. League Gothic: Classic condensed gothic; posters and film titles. Fraunces: Soft, old-style serif; warm stories. Playfair Display: High-contrast serif; elegant titles. Instrument Serif: Narrow editorial serif. DM Serif Display: Bold display serif; quotes and headlines. PT Serif: Classic text serif with italics; reports, history and quotes. Caveat: Quick handwriting; notes and asides. Bangers: Comic-book capitals; reactions and gaming. Luckiest Guy: Fat cartoon capitals; kids, gaming and hooks. JetBrains Mono: Monospace; code, terminals and numbers that line up.
size optionalnumberThe captions' type size, in pixels on a frame whose shorter side is 1080; larger shows fewer words at a time.
rows optionalnumberHow many lines a caption runs to; as many words show at once as fit.
case optionalas_said | capitals | lower_caseCapital letters as said, all capitals, or all lower case.
weight optionalstyle | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900How heavy its letters are; the nearest weight its font has is drawn. style: its style's own
ahead optionalstyle | faded | fullHow the words of a caption not said yet look, in a style that shows a caption's words together. style: as its style shows them faded: faint until each is said full: as clear as the word being said
in optionalstyle | none | fade | rise | popHow each caption comes in. style: the move its style has none: just there fade: comes up from nothing rise: lifts into place pop: springs up with a little overshoot
out optionalfade | noneHow each caption goes as the next comes. fade: a quick fade as the next comes none: just gone
curl -X POST https://app.scrub.app/v1/tools/add_captions \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/make_a_looppriced

Make a loop

Makes a short video that loops: it plays on repeat with no jump, for a background or a post. From a few words, or from an image, which is the clip's first and last frame. The movement has to come back round, like waves, a fire, a flag in the wind or birds circling; something crossing the picture one way cannot loop. 8 seconds at most, without sound. Takes image (file, optional). Returns video. One job: making the image, then filming, then finding the loop. Given image, it skips making the image. $0.21 a second of video + $0.07 an image, up to 8 seconds. quote gives its exact price.

FieldTypeWhat it is
image optionalstringA file_id from create_upload, accepting image/jpeg, image/png, image/webp, up to 20 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
promptstringWhat is in the picture and what moves, in a sentence or a few, such as birds circling over a loch at dusk; English works best. With an image, what moves in it.
model optionalVeo 3.1 Fast | Veo 3.1The model that films it, each at its own price.
aspect_ratio optional16:9 | 9:16Its aspect ratio, width to height.
curl -X POST https://app.scrub.app/v1/tools/make_a_loop \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"…"}'
POST/v1/tools/make_narrated_videopriced

Make a narrated video

A narrated video, such as an explainer or a short with a voiceover, from its timeline. Images and music it describes and has not got are made, the music as long as the video, the script is voiced, and the images, words on screen, captions, logo, sounds and music are drawn into one video, each at its moment. To change a finished video, run it again on the timeline its voice step returned, with the change made: images, music and lines already made are kept and not charged again, so a new image costs only the video and a changed line adds only its voice. Takes timeline (a video's timeline). Returns video, preview, captions (every caption as it is shown, from when it shows until it goes, as an SRT file. None when the video has no captions). One job: making the images, then making the music, then voicing the script, then drawing the video. $0.10 a minute of video + $0.01 per 12 seconds of speech + $0.08 per 30 seconds of music, at least $0.10, at most $0.30 + $0.07 an image, up to 3 minutes. quote gives its exact price.

FieldTypeWhat it is
timelineobjectThe video, scene by scene, with its look; or a script, whose lines become its scenes: text, or one already voiced, as given back voiced, each voiced line kept as it was voiced and not voiced again. A timeline is {version: 2, title?, aspect_ratios: [9:16, 1:1, 4:5, 16:9], brand?: {back, text, accent: colours like #1c1c1c (the accent colours a caption style's key or said word; leave it out to keep captions black and white), font: the font of words on screen that name none, one of Inter Tight, Inter, Manrope, Archivo, Montserrat, Poppins, Figtree, Bricolage Grotesque, Rubik, Bebas Neue, Anton, Oswald, League Gothic, Fraunces, Playfair Display, Instrument Serif, DM Serif Display, PT Serif, Caveat, Bangers, Luckiest Guy, JetBrains Mono}, voice?: {name: one of Achernar (soft, female), Achird (friendly, male), Algenib (gravelly, male), Algieba (smooth, male), Alnilam (firm, male), Aoede (breezy, female), Autonoe (bright, female), Callirrhoe (easy-going, female), Charon (informative, male), Despina (smooth, female), Enceladus (breathy, male), Erinome (clear, female), Fenrir (excitable, male), Gacrux (mature, female), Iapetus (clear, male), Kore (firm, female), Laomedeia (upbeat, female), Leda (youthful, female), Orus (firm, male), Pulcherrima (forward, female), Puck (upbeat, male), Rasalgethi (informative, male), Sadachbia (lively, male), Sadaltager (knowledgeable, male), Schedar (even, male), Sulafat (warm, female), Umbriel (easy-going, male), Vindemiatrix (gentle, female), Zephyr (bright, female), Zubenelgenubi (casual, male)}, captions?: {style: clean (Clean: White, heavy, a few words at a time. Each word lights up as it is said.), pill (Pill: The word being said sits in a white pill, in black.), box (Box: The phrase on a dark rounded box, for busy footage.), bold (Bold: Heavy words that pop in as they are said; the word being said grows.), subtitles (Subtitles: One or two full lines, smaller, without movement: for talks and wide videos.) or off; size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 72 reads like a phone app's captions; larger means fewer words at a time); font?: one of the fonts below (Inter Tight when left out); rows?: 1 or 2 lines a caption runs to (1 when left out); width?: how wide a caption may run, as a share of the frame's shorter side, 0.2 to 3; colour?: a colour like #ffffff; said_colour?: the word being said in Pill, on its pill (near-black when left out); weight?: 100 to 900, the nearest its font has (its style's when left out); ahead?: the words not said yet, style (As its style: as its style shows them), faded (Faded: faint until each is said), full (Full: as clear as the word being said) (style when left out; none to come in a style that shows each word as it is said); outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (the style's own when left out); in?: style (As its style: the move its style has), none (None: just there), fade (Fade: comes up from nothing), rise (Rise: lifts into place), pop (Pop: springs up with a little overshoot) (style when left out); out?: fade (Fade out: a quick fade as the next comes), none (None: just gone) (fade when left out); with a size, each caption is as many words as fit its width in its rows, never ending on a weak word; case: as_said, capitals, lower_case; place?: {x, y}: where every caption sits, its middle as shares of the frame's width and height, 0.05 to 0.95 (above the bottom of the safe area when left out); lines?: [{from, to: the first and last word of one scene's line it shows, each {scene: id, word, nth?}; text?: what it shows instead of those words, such as 58% for fifty-eight percent, at most 120 characters; place?: {x, y} for this one; hide?: true to show nothing for it; hold?: true to stay on screen until the next caption; inline?: true for text shown among the words around it, in their caption, such as a figure for words said, never a caption of its own}]: captions set by hand, none overlapping; every other caption is made from the words said, a few at a time}, pauses?: {longest?: seconds, 0 to 5, that every longer pause plays; at?: [{after: {scene: id, word, nth?}, seconds: 0 to 10}]: a pause after that word set by hand, made where there was none; or {before: {scene: id, word}, seconds} for the quiet before the first word of a line with nothing said before it, from its scene's start}: a pause is a gap of a quarter second or more between two words the voice said, or before a line's first word or after its last where nothing is said before or after it; a line not voiced yet has only the pauses set on it by hand, which play exactly as set once it is voiced; longest leaves the quiet before a first word and after a last word as voiced; shortened, lengthened or removed, the video is that much shorter or longer and everything after it moves with its words, and nothing is voiced again, transition?: cut, blur, fade, push, zoom between images (cut, none, when left out), look?: natural, mono, film, crisp over the images, progress?: true for a thin bar showing how much is left, safe?: {area: usual (TikTok, Reels and Shorts: inside the lines captions and text sit on, clear of what the apps put over a video), tiktok (TikTok: clear of TikTok's buttons, name and caption), reels (Instagram Reels: clear of Instagram's buttons, name and caption), shorts (YouTube Shorts: clear of YouTube's buttons, name and caption), even (Even margin: the same distance from every edge), none (None: only the frame's edges and middle) (usual when left out; TikTok, Reels and Shorts each in 9:16 only, the usual one in other shapes), margin?: for even, a share of the frame's shorter side, 0.01 to 0.25 (0.05 when left out)}: where nothing a person needs to read should sit, as placed gives it in each aspect ratio; keep text boxes, logos and captions inside it, music?: {prompt?: what it sounds like, its mood, instruments and pace, only when the person asks for music (made when it runs, as long as the video), src?: a file_id, from?: prompt when it was made from those words, volume 0 to 1}, scenes: [{id, line: what is said, duration: seconds, hold?: seconds it runs on after its line and its clips end, keys?: one or two words of the line, exactly as written, that carry its meaning}], images: [{id, at, until?, src?: a file_id, kind: image or video, motion: still, push, punch, zoom_in, zoom_out, pan_left, pan_right (still when left out; punch jumps closer on a key word), fit?: whole to show all of an image of another shape on a blurred copy of itself, prompt?: what it shows in a sentence or two (made from those words when it has no src), from?: prompt when it was made from them, make?: false to show the video's colours instead, box?: where it sits when not filling the frame, fades?: true for an image with an until to fade in and out (it is simply there when left out), turn?: degrees turned clockwise (-180 to 180), crop?: {left, top, right, bottom}: shares of the image trimmed from each side, 0 to 0.9, length?: a clip's own seconds (read from its file when a draft is saved), start?: seconds into a clip where it begins, to cut its beginning, to?: seconds into a clip where it stops, to cut it short, volume?: 0 to 1 and fade?: seconds for a clip's own sound}], words: [{id, value: the words shown, at most 200 characters, a new line where it breaks, at, until?, box: {x, y, w}: where the text box sits (a box as below; w is the width its words wrap at), font?: one of Inter Tight (Tight and even; every caption style's face.), Inter (The plainest, most legible sans; explainers and data.), Manrope (Calm, geometric and modern.), Archivo (Sturdy grotesque with real weight; news and how-tos.), Montserrat (The face most short-form captions are set in.), Poppins (Round geometric sans; friendly brands and lifestyle.), Figtree (Clean and warm, a little softer than Inter.), Bricolage Grotesque (Quirky editorial sans.), Rubik (Slightly rounded corners; playful but tidy.), Bebas Neue (Tall capitals; titles, numbers and sport.), Anton (Heavy, narrow capitals; the loudest hook text.), Oswald (Condensed, with weights; lists and labels.), League Gothic (Classic condensed gothic; posters and film titles.), Fraunces (Soft, old-style serif; warm stories.), Playfair Display (High-contrast serif; elegant titles.), Instrument Serif (Narrow editorial serif.), DM Serif Display (Bold display serif; quotes and headlines.), PT Serif (Classic text serif with italics; reports, history and quotes.), Caveat (Quick handwriting; notes and asides.), Bangers (Comic-book capitals; reactions and gaming.), Luckiest Guy (Fat cartoon capitals; kids, gaming and hooks.), JetBrains Mono (Monospace; code, terminals and numbers that line up.), weight?: 400 to 900 (the nearest the font has), italic?: true for its italics where it has them, size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 120 a title, 72 a line, 48 small print), colour?, align?: left, center, right, outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (none and a soft shadow when left out; an outline may also be a colour alone), back?: a colour for a rounded box behind each line, in?: how it comes in, none (None: Just there.), fade (Fade: Comes up from nothing.), rise (Rise: Each word lifts into place.), slide (Slide: Slides in from the left.), pop (Pop: Springs up from small, with a little overshoot.), blur (Blur in: Each word sharpens out of a blur.), typewriter (Typewriter: Typed out, letter by letter.), wipe (Wipe: Revealed from left to right.), word_pop (Word by word: Words pop in one after another.), stomp (Stomp: Slams down from huge with a small shake.), highlighter (Highlighter: A marker swipes in behind the words.), letters (Letter cascade: Letters tumble in one by one.), count (Count up: Its numbers run up to their value.) (none when left out), out?: how it goes, none (None: Just gone.), fade (Fade out: Fades away.), sink (Sink: Each word drops away.), slide_out (Slide out: Slides off to the right.), pop_out (Pop out: Swells a touch, then shrinks to nothing.), blur_out (Blur out: Dissolves into a blur.), wipe_out (Wipe out: Wiped away from left to right.), backspace (Backspace: Deleted letter by letter from the end.) (none when left out)}]: text boxes, inside the safe area unless the person says where; words without a box are from before and sit at a place (top, middle, bottom) and come in by an animation (blur, rise, hook, pop, slide, fade, typewriter), sounds: [{id, src: a file_id, at, until?, volume 0 to 1, fade: seconds it fades in and out}]}. The script is said in one voice, scene after scene; a scene lasts its duration, at least as long as its line takes to say, and a scene with no line is a pause; a clip that begins in a scene, with no until or until the scene's end, plays from its start (or its first frame) to its own end (or its to) and the scene runs on until it does, then for its hold, and everything after moves along. A clip's sound plays, lowered while the voice speaks. Images, words and sounds each begin at their at and end at their until. A moment is {scene: id} (its start, or with end: true its end), {scene: id, word: one word of its line, nth?: 2 for its second time, end?: true when it has been said}, or {item: id of an image, words or sound, end?: true}, any of them with plus?: seconds after it (in tenths). An image with no until lasts until the next image with no until begins; a clip never shows past its own end (or its to); words on screen last for the scene they begin in; a sound lasts its own length. Where images show at the same time, a later one in images is drawn on top of an earlier one; where none shows, the video's colours. A logo, or anything else that stays on screen, is an image with a box from the first scene's start until the last scene's end, small (w about 0.18) in a top corner inside the safe area, unless the person says where. A box is {x, y, w}: its middle as shares of the frame's width and height (0.5, 0.5 is the middle; -1 to 2, so it can hang over an edge) and its width as a share of the frame's shorter side (0.02 to 10); its height follows its own shape, and what is outside the frame is not shown. Ids are short names, each used once. files?: [file_id, …]: files kept with the video and not yet placed in it, so its Media shows them when it is opened again. Files are file_ids from this workspace.
aspect_ratio optional9:16 | 1:1 | 4:5 | 16:9The aspect ratio to make it in, width to height.
part optionalvoice | images | musicOnly this part, made now at its own price, to see or hear it before the rest: voice (Voice a script), images (Generate an image), music (Generate music). The settings come back with it in place and the next part is offered. Left out, all of it is made.
curl -X POST https://app.scrub.app/v1/tools/make_narrated_video \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timeline":{}}'
POST/v1/tools/make_shortpriced

Make a short from a video

A finished short from a stretch of a longer recording (a podcast, a webinar, a talk), its video or its audio alone, from the draft Plan the cutaways lays out: the speaker upright, or from audio a picture the whole way through, captions from what is said, the hook it opens on, and the cutaways on its key points made, then drawn into one video. A cutaway left empty shows the speaker. To change a finished short, run it again on its draft with the change made: cutaways already made are kept and not charged again. Takes timeline (a video's timeline). Returns video, preview, captions (every caption as it is shown, from when it shows until it goes, as an SRT file. None when the video has no captions). One job: making the cutaways, then drawing the video. $0.10 a minute of video + $0.07 an image, up to 3 minutes. quote gives its exact price. For a screen recording, no face on screen (said as screen recording, screen share, screen-share, screencast, shared screen, no face), run plan_cutaways first with speaker 1.5 and cutaway 6. With no face to hold viewers, a cut every second or two keeps it moving.

FieldTypeWhat it is
timelineobjectThe short's draft, as Plan the cutaways lays it out, with any changes made to it. A timeline is {version: 2, title?, aspect_ratios: [9:16, 1:1, 4:5, 16:9], brand?: {back, text, accent: colours like #1c1c1c (the accent colours a caption style's key or said word; leave it out to keep captions black and white), font: the font of words on screen that name none, one of Inter Tight, Inter, Manrope, Archivo, Montserrat, Poppins, Figtree, Bricolage Grotesque, Rubik, Bebas Neue, Anton, Oswald, League Gothic, Fraunces, Playfair Display, Instrument Serif, DM Serif Display, PT Serif, Caveat, Bangers, Luckiest Guy, JetBrains Mono}, voice?: {name: one of Achernar (soft, female), Achird (friendly, male), Algenib (gravelly, male), Algieba (smooth, male), Alnilam (firm, male), Aoede (breezy, female), Autonoe (bright, female), Callirrhoe (easy-going, female), Charon (informative, male), Despina (smooth, female), Enceladus (breathy, male), Erinome (clear, female), Fenrir (excitable, male), Gacrux (mature, female), Iapetus (clear, male), Kore (firm, female), Laomedeia (upbeat, female), Leda (youthful, female), Orus (firm, male), Pulcherrima (forward, female), Puck (upbeat, male), Rasalgethi (informative, male), Sadachbia (lively, male), Sadaltager (knowledgeable, male), Schedar (even, male), Sulafat (warm, female), Umbriel (easy-going, male), Vindemiatrix (gentle, female), Zephyr (bright, female), Zubenelgenubi (casual, male)}, captions?: {style: clean (Clean: White, heavy, a few words at a time. Each word lights up as it is said.), pill (Pill: The word being said sits in a white pill, in black.), box (Box: The phrase on a dark rounded box, for busy footage.), bold (Bold: Heavy words that pop in as they are said; the word being said grows.), subtitles (Subtitles: One or two full lines, smaller, without movement: for talks and wide videos.) or off; size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 72 reads like a phone app's captions; larger means fewer words at a time); font?: one of the fonts below (Inter Tight when left out); rows?: 1 or 2 lines a caption runs to (1 when left out); width?: how wide a caption may run, as a share of the frame's shorter side, 0.2 to 3; colour?: a colour like #ffffff; said_colour?: the word being said in Pill, on its pill (near-black when left out); weight?: 100 to 900, the nearest its font has (its style's when left out); ahead?: the words not said yet, style (As its style: as its style shows them), faded (Faded: faint until each is said), full (Full: as clear as the word being said) (style when left out; none to come in a style that shows each word as it is said); outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (the style's own when left out); in?: style (As its style: the move its style has), none (None: just there), fade (Fade: comes up from nothing), rise (Rise: lifts into place), pop (Pop: springs up with a little overshoot) (style when left out); out?: fade (Fade out: a quick fade as the next comes), none (None: just gone) (fade when left out); with a size, each caption is as many words as fit its width in its rows, never ending on a weak word; case: as_said, capitals, lower_case; place?: {x, y}: where every caption sits, its middle as shares of the frame's width and height, 0.05 to 0.95 (above the bottom of the safe area when left out); lines?: [{from, to: the first and last word of one scene's line it shows, each {scene: id, word, nth?}; text?: what it shows instead of those words, such as 58% for fifty-eight percent, at most 120 characters; place?: {x, y} for this one; hide?: true to show nothing for it; hold?: true to stay on screen until the next caption; inline?: true for text shown among the words around it, in their caption, such as a figure for words said, never a caption of its own}]: captions set by hand, none overlapping; every other caption is made from the words said, a few at a time}, pauses?: {longest?: seconds, 0 to 5, that every longer pause plays; at?: [{after: {scene: id, word, nth?}, seconds: 0 to 10}]: a pause after that word set by hand, made where there was none; or {before: {scene: id, word}, seconds} for the quiet before the first word of a line with nothing said before it, from its scene's start}: a pause is a gap of a quarter second or more between two words the voice said, or before a line's first word or after its last where nothing is said before or after it; a line not voiced yet has only the pauses set on it by hand, which play exactly as set once it is voiced; longest leaves the quiet before a first word and after a last word as voiced; shortened, lengthened or removed, the video is that much shorter or longer and everything after it moves with its words, and nothing is voiced again, transition?: cut, blur, fade, push, zoom between images (cut, none, when left out), look?: natural, mono, film, crisp over the images, progress?: true for a thin bar showing how much is left, safe?: {area: usual (TikTok, Reels and Shorts: inside the lines captions and text sit on, clear of what the apps put over a video), tiktok (TikTok: clear of TikTok's buttons, name and caption), reels (Instagram Reels: clear of Instagram's buttons, name and caption), shorts (YouTube Shorts: clear of YouTube's buttons, name and caption), even (Even margin: the same distance from every edge), none (None: only the frame's edges and middle) (usual when left out; TikTok, Reels and Shorts each in 9:16 only, the usual one in other shapes), margin?: for even, a share of the frame's shorter side, 0.01 to 0.25 (0.05 when left out)}: where nothing a person needs to read should sit, as placed gives it in each aspect ratio; keep text boxes, logos and captions inside it, music?: {prompt?: what it sounds like, its mood, instruments and pace, only when the person asks for music (made when it runs, as long as the video), src?: a file_id, from?: prompt when it was made from those words, volume 0 to 1}, scenes: [{id, line: what is said, duration: seconds, hold?: seconds it runs on after its line and its clips end, keys?: one or two words of the line, exactly as written, that carry its meaning}], images: [{id, at, until?, src?: a file_id, kind: image or video, motion: still, push, punch, zoom_in, zoom_out, pan_left, pan_right (still when left out; punch jumps closer on a key word), fit?: whole to show all of an image of another shape on a blurred copy of itself, prompt?: what it shows in a sentence or two (made from those words when it has no src), from?: prompt when it was made from them, make?: false to show the video's colours instead, box?: where it sits when not filling the frame, fades?: true for an image with an until to fade in and out (it is simply there when left out), turn?: degrees turned clockwise (-180 to 180), crop?: {left, top, right, bottom}: shares of the image trimmed from each side, 0 to 0.9, length?: a clip's own seconds (read from its file when a draft is saved), start?: seconds into a clip where it begins, to cut its beginning, to?: seconds into a clip where it stops, to cut it short, volume?: 0 to 1 and fade?: seconds for a clip's own sound}], words: [{id, value: the words shown, at most 200 characters, a new line where it breaks, at, until?, box: {x, y, w}: where the text box sits (a box as below; w is the width its words wrap at), font?: one of Inter Tight (Tight and even; every caption style's face.), Inter (The plainest, most legible sans; explainers and data.), Manrope (Calm, geometric and modern.), Archivo (Sturdy grotesque with real weight; news and how-tos.), Montserrat (The face most short-form captions are set in.), Poppins (Round geometric sans; friendly brands and lifestyle.), Figtree (Clean and warm, a little softer than Inter.), Bricolage Grotesque (Quirky editorial sans.), Rubik (Slightly rounded corners; playful but tidy.), Bebas Neue (Tall capitals; titles, numbers and sport.), Anton (Heavy, narrow capitals; the loudest hook text.), Oswald (Condensed, with weights; lists and labels.), League Gothic (Classic condensed gothic; posters and film titles.), Fraunces (Soft, old-style serif; warm stories.), Playfair Display (High-contrast serif; elegant titles.), Instrument Serif (Narrow editorial serif.), DM Serif Display (Bold display serif; quotes and headlines.), PT Serif (Classic text serif with italics; reports, history and quotes.), Caveat (Quick handwriting; notes and asides.), Bangers (Comic-book capitals; reactions and gaming.), Luckiest Guy (Fat cartoon capitals; kids, gaming and hooks.), JetBrains Mono (Monospace; code, terminals and numbers that line up.), weight?: 400 to 900 (the nearest the font has), italic?: true for its italics where it has them, size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 120 a title, 72 a line, 48 small print), colour?, align?: left, center, right, outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (none and a soft shadow when left out; an outline may also be a colour alone), back?: a colour for a rounded box behind each line, in?: how it comes in, none (None: Just there.), fade (Fade: Comes up from nothing.), rise (Rise: Each word lifts into place.), slide (Slide: Slides in from the left.), pop (Pop: Springs up from small, with a little overshoot.), blur (Blur in: Each word sharpens out of a blur.), typewriter (Typewriter: Typed out, letter by letter.), wipe (Wipe: Revealed from left to right.), word_pop (Word by word: Words pop in one after another.), stomp (Stomp: Slams down from huge with a small shake.), highlighter (Highlighter: A marker swipes in behind the words.), letters (Letter cascade: Letters tumble in one by one.), count (Count up: Its numbers run up to their value.) (none when left out), out?: how it goes, none (None: Just gone.), fade (Fade out: Fades away.), sink (Sink: Each word drops away.), slide_out (Slide out: Slides off to the right.), pop_out (Pop out: Swells a touch, then shrinks to nothing.), blur_out (Blur out: Dissolves into a blur.), wipe_out (Wipe out: Wiped away from left to right.), backspace (Backspace: Deleted letter by letter from the end.) (none when left out)}]: text boxes, inside the safe area unless the person says where; words without a box are from before and sit at a place (top, middle, bottom) and come in by an animation (blur, rise, hook, pop, slide, fade, typewriter), sounds: [{id, src: a file_id, at, until?, volume 0 to 1, fade: seconds it fades in and out}]}. The script is said in one voice, scene after scene; a scene lasts its duration, at least as long as its line takes to say, and a scene with no line is a pause; a clip that begins in a scene, with no until or until the scene's end, plays from its start (or its first frame) to its own end (or its to) and the scene runs on until it does, then for its hold, and everything after moves along. A clip's sound plays, lowered while the voice speaks. Images, words and sounds each begin at their at and end at their until. A moment is {scene: id} (its start, or with end: true its end), {scene: id, word: one word of its line, nth?: 2 for its second time, end?: true when it has been said}, or {item: id of an image, words or sound, end?: true}, any of them with plus?: seconds after it (in tenths). An image with no until lasts until the next image with no until begins; a clip never shows past its own end (or its to); words on screen last for the scene they begin in; a sound lasts its own length. Where images show at the same time, a later one in images is drawn on top of an earlier one; where none shows, the video's colours. A logo, or anything else that stays on screen, is an image with a box from the first scene's start until the last scene's end, small (w about 0.18) in a top corner inside the safe area, unless the person says where. A box is {x, y, w}: its middle as shares of the frame's width and height (0.5, 0.5 is the middle; -1 to 2, so it can hang over an edge) and its width as a share of the frame's shorter side (0.02 to 10); its height follows its own shape, and what is outside the frame is not shown. Ids are short names, each used once. files?: [file_id, …]: files kept with the video and not yet placed in it, so its Media shows them when it is opened again. Files are file_ids from this workspace.
aspect_ratio optional9:16 | 1:1 | 4:5 | 16:9The aspect ratio to make it in, width to height.
part optionalimagesOnly this part, made now at its own price, to see or hear it before the rest: images (Generate an image). The settings come back with it in place and the next part is offered. Left out, all of it is made.
curl -X POST https://app.scrub.app/v1/tools/make_short \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timeline":{}}'
POST/v1/tools/animate_imagepriced

Animate an image

Turns an image into a short video clip with sound (image to video): it starts on the image and moves as the words describe, 4, 6 or 8 seconds, wide or upright, 720p or 1080p, as an MP4. It can also end on the same image, so the clip plays on repeat with no jump. Takes image (file). Returns video (the clip, an MP4 with sound, at 24 frames a second). $0.15 to $0.80 a second of video: at 720p Recommended $0.15, Veo 3.1 Fast $0.20, Veo 3.1 $0.80; at 1080p Recommended $0.25, Veo 3.1 Fast $0.24, Veo 3.1 $0.80, up to 8 seconds.

FieldTypeWhat it is
imagestringA file_id from create_upload, accepting image/jpeg, image/png, image/webp, up to 20 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
promptstringWhat moves and how, and what is heard, in a sentence or a few; English works best.
end_on_image optionalbooleanThe clip ends on the image it starts on, so it plays on repeat with no jump; only at 8 seconds. Movement has to come back round for that, like waves, a fire or birds circling, not something crossing the picture one way.
model optionalrecommended | Veo 3.1 Fast | Veo 3.1The model that makes it. Recommended is Scrub's pick, Veo 3.1 Lite; the others by name, each at its own price.
aspect_ratio optional16:9 | 9:16Its aspect ratio, width to height. An image of another shape is cut from the middle to fill it, with no bars.
seconds optional4 | 6 | 8How long it runs, in seconds.
resolution optional720p | 1080pIts resolution. 1080p only at 8 seconds.
curl -X POST https://app.scrub.app/v1/tools/animate_image \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image":"w/…/file_id","prompt":"…"}'
POST/v1/tools/assemble_videopriced

Assemble a video

Draws a video from its timeline: the voice, images, words on screen, captions, logo, sounds and music, each at its moment, upright, square, portrait or wide. A reading, a picture or music already made goes in as it is, as a sound or an image, so nothing is made twice. Takes timeline (a video's timeline). Returns video, preview, captions (every caption as it is shown, from when it shows until it goes, as an SRT file. None when the video has no captions). $0.10 a minute of video, up to 3 minutes.

FieldTypeWhat it is
timelineobjectThe video's timeline, as the builder shows it. A timeline is {version: 2, title?, aspect_ratios: [9:16, 1:1, 4:5, 16:9], brand?: {back, text, accent: colours like #1c1c1c (the accent colours a caption style's key or said word; leave it out to keep captions black and white), font: the font of words on screen that name none, one of Inter Tight, Inter, Manrope, Archivo, Montserrat, Poppins, Figtree, Bricolage Grotesque, Rubik, Bebas Neue, Anton, Oswald, League Gothic, Fraunces, Playfair Display, Instrument Serif, DM Serif Display, PT Serif, Caveat, Bangers, Luckiest Guy, JetBrains Mono}, voice?: {name: one of Achernar (soft, female), Achird (friendly, male), Algenib (gravelly, male), Algieba (smooth, male), Alnilam (firm, male), Aoede (breezy, female), Autonoe (bright, female), Callirrhoe (easy-going, female), Charon (informative, male), Despina (smooth, female), Enceladus (breathy, male), Erinome (clear, female), Fenrir (excitable, male), Gacrux (mature, female), Iapetus (clear, male), Kore (firm, female), Laomedeia (upbeat, female), Leda (youthful, female), Orus (firm, male), Pulcherrima (forward, female), Puck (upbeat, male), Rasalgethi (informative, male), Sadachbia (lively, male), Sadaltager (knowledgeable, male), Schedar (even, male), Sulafat (warm, female), Umbriel (easy-going, male), Vindemiatrix (gentle, female), Zephyr (bright, female), Zubenelgenubi (casual, male)}, captions?: {style: clean (Clean: White, heavy, a few words at a time. Each word lights up as it is said.), pill (Pill: The word being said sits in a white pill, in black.), box (Box: The phrase on a dark rounded box, for busy footage.), bold (Bold: Heavy words that pop in as they are said; the word being said grows.), subtitles (Subtitles: One or two full lines, smaller, without movement: for talks and wide videos.) or off; size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 72 reads like a phone app's captions; larger means fewer words at a time); font?: one of the fonts below (Inter Tight when left out); rows?: 1 or 2 lines a caption runs to (1 when left out); width?: how wide a caption may run, as a share of the frame's shorter side, 0.2 to 3; colour?: a colour like #ffffff; said_colour?: the word being said in Pill, on its pill (near-black when left out); weight?: 100 to 900, the nearest its font has (its style's when left out); ahead?: the words not said yet, style (As its style: as its style shows them), faded (Faded: faint until each is said), full (Full: as clear as the word being said) (style when left out; none to come in a style that shows each word as it is said); outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (the style's own when left out); in?: style (As its style: the move its style has), none (None: just there), fade (Fade: comes up from nothing), rise (Rise: lifts into place), pop (Pop: springs up with a little overshoot) (style when left out); out?: fade (Fade out: a quick fade as the next comes), none (None: just gone) (fade when left out); with a size, each caption is as many words as fit its width in its rows, never ending on a weak word; case: as_said, capitals, lower_case; place?: {x, y}: where every caption sits, its middle as shares of the frame's width and height, 0.05 to 0.95 (above the bottom of the safe area when left out); lines?: [{from, to: the first and last word of one scene's line it shows, each {scene: id, word, nth?}; text?: what it shows instead of those words, such as 58% for fifty-eight percent, at most 120 characters; place?: {x, y} for this one; hide?: true to show nothing for it; hold?: true to stay on screen until the next caption; inline?: true for text shown among the words around it, in their caption, such as a figure for words said, never a caption of its own}]: captions set by hand, none overlapping; every other caption is made from the words said, a few at a time}, pauses?: {longest?: seconds, 0 to 5, that every longer pause plays; at?: [{after: {scene: id, word, nth?}, seconds: 0 to 10}]: a pause after that word set by hand, made where there was none; or {before: {scene: id, word}, seconds} for the quiet before the first word of a line with nothing said before it, from its scene's start}: a pause is a gap of a quarter second or more between two words the voice said, or before a line's first word or after its last where nothing is said before or after it; a line not voiced yet has only the pauses set on it by hand, which play exactly as set once it is voiced; longest leaves the quiet before a first word and after a last word as voiced; shortened, lengthened or removed, the video is that much shorter or longer and everything after it moves with its words, and nothing is voiced again, transition?: cut, blur, fade, push, zoom between images (cut, none, when left out), look?: natural, mono, film, crisp over the images, progress?: true for a thin bar showing how much is left, safe?: {area: usual (TikTok, Reels and Shorts: inside the lines captions and text sit on, clear of what the apps put over a video), tiktok (TikTok: clear of TikTok's buttons, name and caption), reels (Instagram Reels: clear of Instagram's buttons, name and caption), shorts (YouTube Shorts: clear of YouTube's buttons, name and caption), even (Even margin: the same distance from every edge), none (None: only the frame's edges and middle) (usual when left out; TikTok, Reels and Shorts each in 9:16 only, the usual one in other shapes), margin?: for even, a share of the frame's shorter side, 0.01 to 0.25 (0.05 when left out)}: where nothing a person needs to read should sit, as placed gives it in each aspect ratio; keep text boxes, logos and captions inside it, music?: {prompt?: what it sounds like, its mood, instruments and pace, only when the person asks for music (made when it runs, as long as the video), src?: a file_id, from?: prompt when it was made from those words, volume 0 to 1}, scenes: [{id, line: what is said, duration: seconds, hold?: seconds it runs on after its line and its clips end, keys?: one or two words of the line, exactly as written, that carry its meaning}], images: [{id, at, until?, src?: a file_id, kind: image or video, motion: still, push, punch, zoom_in, zoom_out, pan_left, pan_right (still when left out; punch jumps closer on a key word), fit?: whole to show all of an image of another shape on a blurred copy of itself, prompt?: what it shows in a sentence or two (made from those words when it has no src), from?: prompt when it was made from them, make?: false to show the video's colours instead, box?: where it sits when not filling the frame, fades?: true for an image with an until to fade in and out (it is simply there when left out), turn?: degrees turned clockwise (-180 to 180), crop?: {left, top, right, bottom}: shares of the image trimmed from each side, 0 to 0.9, length?: a clip's own seconds (read from its file when a draft is saved), start?: seconds into a clip where it begins, to cut its beginning, to?: seconds into a clip where it stops, to cut it short, volume?: 0 to 1 and fade?: seconds for a clip's own sound}], words: [{id, value: the words shown, at most 200 characters, a new line where it breaks, at, until?, box: {x, y, w}: where the text box sits (a box as below; w is the width its words wrap at), font?: one of Inter Tight (Tight and even; every caption style's face.), Inter (The plainest, most legible sans; explainers and data.), Manrope (Calm, geometric and modern.), Archivo (Sturdy grotesque with real weight; news and how-tos.), Montserrat (The face most short-form captions are set in.), Poppins (Round geometric sans; friendly brands and lifestyle.), Figtree (Clean and warm, a little softer than Inter.), Bricolage Grotesque (Quirky editorial sans.), Rubik (Slightly rounded corners; playful but tidy.), Bebas Neue (Tall capitals; titles, numbers and sport.), Anton (Heavy, narrow capitals; the loudest hook text.), Oswald (Condensed, with weights; lists and labels.), League Gothic (Classic condensed gothic; posters and film titles.), Fraunces (Soft, old-style serif; warm stories.), Playfair Display (High-contrast serif; elegant titles.), Instrument Serif (Narrow editorial serif.), DM Serif Display (Bold display serif; quotes and headlines.), PT Serif (Classic text serif with italics; reports, history and quotes.), Caveat (Quick handwriting; notes and asides.), Bangers (Comic-book capitals; reactions and gaming.), Luckiest Guy (Fat cartoon capitals; kids, gaming and hooks.), JetBrains Mono (Monospace; code, terminals and numbers that line up.), weight?: 400 to 900 (the nearest the font has), italic?: true for its italics where it has them, size?: a type size, 16 to 400, pixels on a frame whose shorter side is 1080 (about 120 a title, 72 a line, 48 small print), colour?, align?: left, center, right, outline?: false for none, or {colour?, width?: 0 to 30, percent of the type size}; shadow?: false for none, or {colour?, opacity?: 0 to 100 percent, blur?: 0 to 100 and distance?: 0 to 50, percent of the type size, the shadow dropped straight down} (none and a soft shadow when left out; an outline may also be a colour alone), back?: a colour for a rounded box behind each line, in?: how it comes in, none (None: Just there.), fade (Fade: Comes up from nothing.), rise (Rise: Each word lifts into place.), slide (Slide: Slides in from the left.), pop (Pop: Springs up from small, with a little overshoot.), blur (Blur in: Each word sharpens out of a blur.), typewriter (Typewriter: Typed out, letter by letter.), wipe (Wipe: Revealed from left to right.), word_pop (Word by word: Words pop in one after another.), stomp (Stomp: Slams down from huge with a small shake.), highlighter (Highlighter: A marker swipes in behind the words.), letters (Letter cascade: Letters tumble in one by one.), count (Count up: Its numbers run up to their value.) (none when left out), out?: how it goes, none (None: Just gone.), fade (Fade out: Fades away.), sink (Sink: Each word drops away.), slide_out (Slide out: Slides off to the right.), pop_out (Pop out: Swells a touch, then shrinks to nothing.), blur_out (Blur out: Dissolves into a blur.), wipe_out (Wipe out: Wiped away from left to right.), backspace (Backspace: Deleted letter by letter from the end.) (none when left out)}]: text boxes, inside the safe area unless the person says where; words without a box are from before and sit at a place (top, middle, bottom) and come in by an animation (blur, rise, hook, pop, slide, fade, typewriter), sounds: [{id, src: a file_id, at, until?, volume 0 to 1, fade: seconds it fades in and out}]}. The script is said in one voice, scene after scene; a scene lasts its duration, at least as long as its line takes to say, and a scene with no line is a pause; a clip that begins in a scene, with no until or until the scene's end, plays from its start (or its first frame) to its own end (or its to) and the scene runs on until it does, then for its hold, and everything after moves along. A clip's sound plays, lowered while the voice speaks. Images, words and sounds each begin at their at and end at their until. A moment is {scene: id} (its start, or with end: true its end), {scene: id, word: one word of its line, nth?: 2 for its second time, end?: true when it has been said}, or {item: id of an image, words or sound, end?: true}, any of them with plus?: seconds after it (in tenths). An image with no until lasts until the next image with no until begins; a clip never shows past its own end (or its to); words on screen last for the scene they begin in; a sound lasts its own length. Where images show at the same time, a later one in images is drawn on top of an earlier one; where none shows, the video's colours. A logo, or anything else that stays on screen, is an image with a box from the first scene's start until the last scene's end, small (w about 0.18) in a top corner inside the safe area, unless the person says where. A box is {x, y, w}: its middle as shares of the frame's width and height (0.5, 0.5 is the middle; -1 to 2, so it can hang over an edge) and its width as a share of the frame's shorter side (0.02 to 10); its height follows its own shape, and what is outside the frame is not shown. Ids are short names, each used once. files?: [file_id, …]: files kept with the video and not yet placed in it, so its Media shows them when it is opened again. Files are file_ids from this workspace.
aspect_ratio optional9:16 | 1:1 | 4:5 | 16:9The aspect ratio it is drawn in, width to height. 9:16 is upright for stories and reels, 1:1 square, 4:5 portrait for feeds, 16:9 wide.
curl -X POST https://app.scrub.app/v1/tools/assemble_video \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timeline":{}}'
POST/v1/tools/burn_captionspriced

Burn captions into a video

Draws captions onto a video's picture (burned-in subtitles), from a subtitle file, captions written out or one line of text, in the place, style and size chosen. Takes src (file), subtitle_file (file). Give exactly one of: text, start and end; or captions; or subtitle_file. Returns video, preview, edl (the captions as drawn, each with its words, times, place and style, and the source they were drawn on). $0.10 a minute of video, up to 60 minutes. Afterwards, change start, end, position, text or captions with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 2 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
subtitle_file optionalstringA file_id from create_upload, accepting application/vnd.scrub.timed-text+json, application/json, application/x-subrip, text/vtt, text/plain, up to 16 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, transcribe (Transcribe a video) from the file given as src makes the transcript_file that convert (Convert a transcript) with as json makes one from: run them in that order first and pass the last one's job_id here.
text optionalstringOne caption's text, with start and end.
start optionalnumberWhen that one caption appears, in seconds.
end optionalnumberGreater than start.
captions optionalarrayCaptions written out, each with start and end in seconds and its text.
position optionallower_third | centre | top_left
style optionalclean | pill | box | bold | subtitlesHow the captions look. clean: White, heavy, a few words at a time. Each word lights up as it is said. pill: The word being said sits in a white pill, in black. box: The phrase on a dark rounded box, for busy footage. bold: Heavy words that pop in as they are said; the word being said grows. subtitles: One or two full lines, smaller, without movement: for talks and wide videos.
font optionalInter Tight | Inter | Manrope | Archivo | Montserrat | Poppins | Figtree | Bricolage Grotesque | Rubik | Bebas Neue | Anton | Oswald | League Gothic | Fraunces | Playfair Display | Instrument Serif | DM Serif Display | PT Serif | Caveat | Bangers | Luckiest Guy | JetBrains MonoThe font the captions are set in. Inter Tight: Tight and even; every caption style's face. Inter: The plainest, most legible sans; explainers and data. Manrope: Calm, geometric and modern. Archivo: Sturdy grotesque with real weight; news and how-tos. Montserrat: The face most short-form captions are set in. Poppins: Round geometric sans; friendly brands and lifestyle. Figtree: Clean and warm, a little softer than Inter. Bricolage Grotesque: Quirky editorial sans. Rubik: Slightly rounded corners; playful but tidy. Bebas Neue: Tall capitals; titles, numbers and sport. Anton: Heavy, narrow capitals; the loudest hook text. Oswald: Condensed, with weights; lists and labels. League Gothic: Classic condensed gothic; posters and film titles. Fraunces: Soft, old-style serif; warm stories. Playfair Display: High-contrast serif; elegant titles. Instrument Serif: Narrow editorial serif. DM Serif Display: Bold display serif; quotes and headlines. PT Serif: Classic text serif with italics; reports, history and quotes. Caveat: Quick handwriting; notes and asides. Bangers: Comic-book capitals; reactions and gaming. Luckiest Guy: Fat cartoon capitals; kids, gaming and hooks. JetBrains Mono: Monospace; code, terminals and numbers that line up.
size optionalnumberThe captions' type size, in pixels on a frame whose shorter side is 1080; larger shows fewer words at a time.
rows optionalnumberHow many lines a caption runs to; as many words show at once as fit.
case optionalas_said | capitals | lower_caseCapital letters as said, all capitals, or all lower case.
weight optionalstyle | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900How heavy its letters are; the nearest weight its font has is drawn. style: its style's own
ahead optionalstyle | faded | fullHow the words of a caption not said yet look, in a style that shows a caption's words together. style: as its style shows them faded: faint until each is said full: as clear as the word being said
in optionalstyle | none | fade | rise | popHow each caption comes in. style: the move its style has none: just there fade: comes up from nothing rise: lifts into place pop: springs up with a little overshoot
out optionalfade | noneHow each caption goes as the next comes. fade: a quick fade as the next comes none: just gone
curl -X POST https://app.scrub.app/v1/tools/burn_captions \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/compress_videopriced

Compress a video

Makes a video file smaller to send or upload: looking the same, much smaller with no difference most people notice, or as small as it goes. Takes src (file). Returns video. $0.02 a minute of video, up to 30 minutes. Afterwards, change quality with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 2 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Or a file_id (cf_…) from list_computers: a file on the person’s computer, run there by the Scrub app while it is open, at the price there.
quality optionallooks_the_same | balanced | smallestHow much to keep. Looks the same (looks_the_same) is hard to tell from the original; Balanced (balanced) is much smaller, with no difference most people notice; Smallest (smallest) is smallest, a little softer. Leave it out unless the person said which: they are asked, each choice priced with what it comes to.
curl -X POST https://app.scrub.app/v1/tools/compress_video \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/convertpriced

Convert a transcript

Writes a transcript as subtitles (SRT or VTT), text, Markdown, HTML or JSON, grouped by caption, sentence, paragraph or word, from a transcription or a transcript or subtitle file; the whole of it, or a stretch timed from its start. Takes transcript_file (file). Returns file. Free. Afterwards, change as or by with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
transcript_filestringA file_id from create_upload, accepting application/vnd.scrub.transcript+json, application/json, application/x-subrip, text/vtt, text/plain, up to 16 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, transcribe (Transcribe a video) makes one, or transcribe_audio (Transcribe audio) makes one: run it first and pass its job_id here.
as optionaltext | markdown | html | json | srt | vttHow it is written. srt and vtt are subtitle files; markdown and html carry a timestamp on each part.
by optionalauto | caption | sentence | paragraph | wordHow words are grouped. auto is caption for srt and vtt, paragraph for text, markdown and html, and word for json. Captions are at most two lines of 42 characters and seven seconds.
start optionalnumberSeconds into the transcript where the stretch begins; its words are timed from here.
end optionalnumberSeconds into the transcript where the stretch ends; at 0 it runs to the end.
curl -X POST https://app.scrub.app/v1/tools/convert \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transcript_file":"w/…/file_id"}'
POST/v1/tools/extract_audiopriced

Extract audio from a video

Extracts the audio from a video as an audio file (M4A), the whole length, copied untouched when it can be. Useful on its own, and much smaller to send than the video when only the sound is needed, such as to transcribe it. A video with no sound is refused. Takes src (file). Returns audio. $0.01 an hour of video, up to 6 hours.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 4 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Or a file_id (cf_…) from list_computers: a file on the person’s computer, run there by the Scrub app while it is open, at the price there.
curl -X POST https://app.scrub.app/v1/tools/extract_audio \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/find_momentspriced

Find the moments

Finds the moments in a recording that stand alone as clips, from its transcription: up to so many or exactly so many, each a run of whole sentences between a shortest and a longest length, with a title, when it is said, its opening words and why it stands alone, as a table. Takes transcript (file). Returns moments (the moments, one row each in the order they are said, with its title, when it starts and ends, how many seconds it runs, its opening line and why it stands alone). $0.10 an hour of recording, up to 10 hours.

FieldTypeWhat it is
transcriptstringA file_id from create_upload, accepting application/vnd.scrub.transcript+json, application/json, up to 16 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, transcribe (Transcribe a video) makes one, or transcribe_audio (Transcribe audio) makes one: run it first and pass its job_id here.
moments optionalnumberThe most moments it finds; fewer when fewer stand alone.
exactly optionalbooleanFinds exactly that many moments, where the recording holds them.
shortest optionalnumberThe shortest a moment runs, in seconds.
longest optionalnumberGreater than shortest.
curl -X POST https://app.scrub.app/v1/tools/find_moments \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transcript":"w/…/file_id"}'
POST/v1/tools/generate_imagepriced

Generate an image

Creates an image from a description (text to image), upright, square, portrait or wide, as a JPEG. Reference images, optional, keep a person, a product, a place or a look from images you give (image to image). Takes prompt (text), references (a list of files, up to 3, optional). Returns image (the image, a JPEG). $0.07 to $0.27 an image: Recommended $0.07, Nano Banana 2 $0.14, Nano Banana Pro $0.27, GPT Image 2 $0.11. Recommended picks by what the words ask: for words in the picture Nano Banana Pro; for a drawn style Recommended; for many people moving GPT Image 2; for people and faces GPT Image 2; for a product shot Recommended; for a landscape Recommended.

FieldTypeWhat it is
references optionalarrayA list: A file_id from create_upload, accepting image/jpeg, image/png, image/webp, up to 20 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. All of them are one job.
promptstringWhat the image shows and how it looks, in a sentence or a few; any language. It has no words in it unless you ask for them.
model optionalrecommended | Nano Banana 2 | Nano Banana Pro | GPT Image 2The model that draws it. Recommended is Scrub's pick for what the words ask, the best scored for the price; the others by name, each at its own price. Recommended may run Nano Banana Pro at $0.27 an image or GPT Image 2 at $0.11 an image, by what the words ask. The words choose the model, and so the price: quote with the exact words you will run.
aspect_ratio optional9:16 | 1:1 | 4:5 | 16:9Its aspect ratio, width to height.
goes_with optionalstringFor a video, what goes with the line the prompt says, such as what the video is about. Scrub picks what to show, never words. Empty, it draws the prompt.
curl -X POST https://app.scrub.app/v1/tools/generate_image \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"…"}'
POST/v1/tools/generate_musicpriced

Generate music

Composes music from a description of its mood, instruments and pace, up to three minutes, as an MP3: instrumental unless singing is asked for, or a song sung with given lyrics. Takes prompt (text), lyrics (text). Returns music (the music, an MP3). $0.08 per 30 seconds of music, at least $0.10, at most $0.30, up to 180 seconds.

FieldTypeWhat it is
promptstringThe music in a sentence or a few, such as its mood, instruments, pace and who sings; any language. Instrumental unless you ask for singing or give lyrics.
lyrics optionalstringWords to sing, in any language, its parts marked like [Verse] and [Chorus] if you like; sung as written. Without them it is instrumental, unless the prompt asks for singing, when lyrics are written for it.
seconds optionalnumberHow long it runs, in seconds, up to three minutes; it ends in a fade.
curl -X POST https://app.scrub.app/v1/tools/generate_music \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"…"}'
POST/v1/tools/generate_videopriced

Generate a video

Creates a short video clip with sound from a description (text to video), 4, 6 or 8 seconds, wide or upright, 720p or 1080p, as an MP4. Reference images, optional, keep a person, a product, a place or a look from images you give in the clip. Takes prompt (text), references (a list of files, up to 3, optional, with model Veo 3.1 Fast or Veo 3.1, with seconds 8). Returns video (the clip, an MP4 with sound, at 24 frames a second). $0.15 to $0.80 a second of video: at 720p Recommended $0.15, Veo 3.1 Fast $0.20, Veo 3.1 $0.80; at 1080p Recommended $0.25, Veo 3.1 Fast $0.24, Veo 3.1 $0.80, up to 8 seconds. Recommended picks by what the words ask: for speech and sound Recommended; for words in the picture Veo 3.1 Fast; for a drawn style Recommended; for many people moving Recommended (no model does it well yet); for people and faces Recommended; for a product shot Recommended (no model does it well yet); for a landscape Recommended.

FieldTypeWhat it is
references optionalarrayA list: A file_id from create_upload, accepting image/jpeg, image/png, image/webp, up to 20 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. All of them are one job.
promptstringWhat happens, how it looks and what is heard, in a sentence or a few; English works best. Describe how a place or thing looks rather than naming it, since a named landmark is drawn as the model imagines it.
model optionalrecommended | Veo 3.1 Fast | Veo 3.1The model that makes it. Recommended is Scrub's pick for what the words ask, the best scored for the price; the others by name, each at its own price. Recommended may run Veo 3.1 Fast at $0.20 a second, by what the words ask. The words choose the model, and so the price: quote with the exact words you will run.
aspect_ratio optional16:9 | 9:16Its aspect ratio, width to height.
seconds optional4 | 6 | 8How long it runs, in seconds.
resolution optional720p | 1080pIts resolution. 1080p only at 8 seconds.
curl -X POST https://app.scrub.app/v1/tools/generate_video \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"…"}'
POST/v1/tools/make_looppriced

Make it loop

Makes a loop of a video from its own frames that plays on repeat with no jump, for a background or a post. It finds where the picture and its motion come back to the start and keeps the frames between, without sound. A video whose movement never comes back, like something crossing the picture one way, is refused. Takes src (file). Returns video. $0.01 a second of video, up to 60 seconds.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 512 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, animate_image (Animate an image) makes one, or generate_video (Generate a video) makes one: run it first and pass its job_id here.
curl -X POST https://app.scrub.app/v1/tools/make_loop \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/make_spreadsheetpriced

Make a spreadsheet

Writes a table as an Excel file and a CSV, from a table another action read, a CSV or JSON file, or rows typed or pasted. Takes table_file (file). Give exactly one of: table_file; or table_text. Returns excel, csv, table (the table as written into the files). Free. Afterwards, change columns or sheet with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
table_file optionalstringA file_id from create_upload, accepting application/vnd.scrub.table+json, text/csv, text/tab-separated-values, application/json, text/plain, up to 20 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, read_fields (Read documents into a table) makes one: run it first and pass its job_id here.
table_text optionalstringA table typed or pasted, one row a line, values separated by commas or tabs, the first line the column names.
columns optionalstringWhich columns to include, in order, by name, comma separated. Leave it empty for every column.
sheet optionalstringThe name of the sheet in the Excel file.
curl -X POST https://app.scrub.app/v1/tools/make_spreadsheet \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/plan_cutawayspriced

Plan the cutaways

Lays a stretch of a recording out as a short to finish, from its transcription: the stretch found by its times or words, the speaker upright and centred (from audio alone, pictures throughout), cutaways on its key points saying what each shows, callouts on figures said, a hook. Changes no word said and makes no image. Takes src (file), transcript (file). Returns timeline (the short as a timeline to change before it is made, upright, its lines said in the recording, the recording under everything (from audio alone, the cutaways one after another instead), each cutaway on its words saying what it shows, each callout a text box on its words, and its hook, its title or nothing on top, as asked). $0.07 a plan.

FieldTypeWhat it is
src optionalstringA file_id from create_upload, accepting video/*, audio/*, up to 4 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
transcript optionalstringA file_id from create_upload, accepting application/vnd.scrub.transcript+json, application/json, up to 16 MB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. Without one, transcribe (Transcribe a video) from the file given as src when it is a video makes one, or transcribe_audio (Transcribe audio) from the file given as src when it is an audio file makes one: run it first and pass its job_id here.
momentstringWhich stretch, by its start and end (8:48 to 9:35) or by what is said in it, and what it is about; a title it gives names the short. Or one row of a table: find_moments (Find the moments) from the file given as transcript makes the table; run it, then give one row's words here, every column by its label.
top optionalhook_at_the_start | title_throughout | noneWhat sits at the top of the short. Hook at the start is a line that makes someone stop scrolling, for its first 3 seconds; Title throughout names it the whole way through; None leaves the top clear.
quiet optionalnumberAny quiet longer than this, in seconds, is cut down to half a second (or to this, when shorter), from the sound and the picture together.
speaker optionalnumberThe longest the speaker is on screen before a cutaway, in seconds. From audio alone nobody is on screen, and every stretch is a cutaway.
cutaway optionalnumberThe longest a cutaway shows, in seconds; from audio alone, the longest each picture shows.
callouts optionalnumberThe most callouts, each a figure or a short phrase said (at most five words), shown big on the words that say it; 0 for none.
curl -X POST https://app.scrub.app/v1/tools/plan_cutaways \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"moment":"w/…/file_id"}'
POST/v1/tools/read_fieldspriced

Read documents into a table

Reads photos, scans or PDFs into a table (data extraction), one row per item found, with the columns asked for or chosen from what is printed. Takes documents (a list of files, up to 200). Returns rows (the table read, one row per item found, with the file and page each came from). $0.05 a page, up to 200 pages. Afterwards, change fields or row with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
documentsarrayA list: A file_id from create_upload, accepting image/jpeg, image/png, image/webp, image/heic, image/heif, application/pdf, up to 14 MB a photo, 30 MB a PDF. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. All of them are one job.
fields optionalarrayWhat to read, each a column of the table. A field has a name, what it means when the name is not enough, and the kind of value it is (text, number, amount, currency, date, or one of a list of choices). Left empty, the columns are chosen from what is printed on the documents.
row optionalstringLeft as document, every document is read, and each row says what kind it is. Name a kind, such as invoice, or several joined by or, and anything else is left out and not charged. Several on one page, or one running over several pages, are each one row.
curl -X POST https://app.scrub.app/v1/tools/read_fields \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"documents":["w/…/file_id"]}'
POST/v1/tools/read_textpriced

Read documents as text

Reads the text of photos, scans and PDFs (OCR), every page, each headed by its file and page. Takes documents (a list of files, up to 200). Returns text (the text of every page, each headed by its file and page), pages. $0.01 a page, up to 200 pages.

FieldTypeWhat it is
documentsarrayA list: A file_id from create_upload, accepting image/jpeg, image/png, image/webp, image/heic, image/heif, application/pdf, up to 20 MB a photo, 30 MB a PDF. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result. All of them are one job.
curl -X POST https://app.scrub.app/v1/tools/read_text \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"documents":["w/…/file_id"]}'
POST/v1/tools/text_to_speechpriced

Text to speech

Generates speech from text (text to speech, TTS): the words read aloud as one audio file, such as a voiceover or narration, in a chosen voice, male or female, from soft to firm, as one take. Takes text (text). Returns speech (the text spoken, as one file). $0.01 per 12 seconds of speech, up to 240 seconds.

FieldTypeWhat it is
textstringWhat is said, exactly as written, as one take; any language.
voice optionalAchernar | Achird | Algenib | Algieba | Alnilam | Aoede | Autonoe | Callirrhoe | Charon | Despina | Enceladus | Erinome | Fenrir | Gacrux | Iapetus | Kore | Laomedeia | Leda | Orus | Pulcherrima | Puck | Rasalgethi | Sadachbia | Sadaltager | Schedar | Sulafat | Umbriel | Vindemiatrix | Zephyr | ZubenelgenubiWho says it. No voice takes an accent, Scottish or other. Achernar: soft, female Achird: friendly, male Algenib: gravelly, male Algieba: smooth, male Alnilam: firm, male Aoede: breezy, female Autonoe: bright, female Callirrhoe: easy-going, female Charon: informative, male Despina: smooth, female Enceladus: breathy, male Erinome: clear, female Fenrir: excitable, male Gacrux: mature, female Iapetus: clear, male Kore: firm, female Laomedeia: upbeat, female Leda: youthful, female Orus: firm, male Pulcherrima: forward, female Puck: upbeat, male Rasalgethi: informative, male Sadachbia: lively, male Sadaltager: knowledgeable, male Schedar: even, male Sulafat: warm, female Umbriel: easy-going, male Vindemiatrix: gentle, female Zephyr: bright, female Zubenelgenubi: casual, male
curl -X POST https://app.scrub.app/v1/tools/text_to_speech \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"…"}'
POST/v1/tools/transcribepriced

Transcribe a video

Transcribes a video: writes down what is said in it, every word with its time (speech to text), in the language it detects or the one given. Takes src (file). Returns transcript. $0.15 per 10 minutes of audio, up to 360 minutes. Afterwards, change language with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting video/*, up to 4 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
language optionalstringLeft as auto, the language is detected. To choose one, give its code, such as en, fr or de.
curl -X POST https://app.scrub.app/v1/tools/transcribe \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/transcribe_audiopriced

Transcribe audio

Transcribes audio, such as a call, a podcast or a voice note: writes down what is said, every word with its time (speech to text), in the language it detects or the one given. Takes src (file). Returns transcript. $0.15 per 10 minutes of audio, up to 360 minutes. Afterwards, change language with adjust; quote_adjustment prices the change.

FieldTypeWhat it is
srcstringA file_id from create_upload, accepting audio/*, up to 4 GB. Get one with create_upload. Or the job_id of a finished job that made a file of this kind: that file is used. Pass the job_id rather than retyping its result.
language optionalstringLeft as auto, the language is detected. To choose one, give its code, such as en, fr or de.
curl -X POST https://app.scrub.app/v1/tools/transcribe_audio \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"src":"w/…/file_id"}'
POST/v1/tools/voice_scriptpriced

Voice a script

Voices a script line by line in a chosen voice, as one take with every word's time: each line's stretch of the audio, a changed line voiced again on its own, lines timed to subtitles or a video. Takes script (text). Returns voice (the voice as one file, each line where it plays), script (the script with each line's stretch of the take, where it plays in the voice file and how long, and every word's time; given again with a line changed, only that line is voiced), timeline (given a video's timeline, the same timeline with each scene's stretch of the take, its length and its word timings set. None when given a script). $0.01 per 12 seconds of speech, up to 240 seconds.

FieldTypeWhat it is
scriptanyWhat is said, line by line, as one take in its voice: text, each paragraph a line (each line when no blank lines part them); a subtitle file's text, SRT or VTT, each cue a line that starts at its time; {lines: [{id, text, at?, until?}], voice?: {name}, timing?: cues or straight}, at and until in seconds; or a video's timeline, whose scenes are its lines. A line voiced before, as it reads and in the same voice, is reused; words moved between lines of one take are cut from it again, not voiced. A script is text, or {lines: [{id, text, at?, until?}], voice?: {name: one of Achernar (soft, female), Achird (friendly, male), Algenib (gravelly, male), Algieba (smooth, male), Alnilam (firm, male), Aoede (breezy, female), Autonoe (bright, female), Callirrhoe (easy-going, female), Charon (informative, male), Despina (smooth, female), Enceladus (breathy, male), Erinome (clear, female), Fenrir (excitable, male), Gacrux (mature, female), Iapetus (clear, male), Kore (firm, female), Laomedeia (upbeat, female), Leda (youthful, female), Orus (firm, male), Pulcherrima (forward, female), Puck (upbeat, male), Rasalgethi (informative, male), Sadachbia (lively, male), Sadaltager (knowledgeable, male), Schedar (even, male), Sulafat (warm, female), Umbriel (easy-going, male), Vindemiatrix (gentle, female), Zephyr (bright, female), Zubenelgenubi (casual, male)}, timing?: cues (each line starts at its at, the default) or straight (one after another)}; {text, voice?, timing?} reads text as given. A subtitle file's text (SRT or VTT) is read as one line for each cue, at its times. Ids are short names, each used once; a line given back voiced keeps its recording, so send it back as it came to voice only what changed.
lines optionalstringSome of the script's lines, by id, comma-separated, to voice only those, each on its own; every other line keeps the voice it has, a changed one still waiting to be voiced. Empty, every line that needs it is voiced.
curl -X POST https://app.scrub.app/v1/tools/voice_script \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"script":"w/…/file_id"}'
POST/v1/tools/write_scriptpriced

Draft a video script

Drafts a short video from an idea, or divides a script given into scenes: what is said, the words on screen, the images and the music, to change before it is voiced and made. Takes idea (text). Returns timeline (the draft as a timeline, scene by scene, each with what is said and the words on screen, and its images, each from a moment of the line with what it shows, and the music it asks for, in words, to change before it is voiced and made). $0.05 a draft.

FieldTypeWhat it is
ideastringWhat the video is about, in a sentence or a few; or a script, whose words are kept as they are, divided into scenes, in its own language. Music it asks for is described in the draft.
seconds optionalnumberAbout how long the video runs, in seconds.
curl -X POST https://app.scrub.app/v1/tools/write_script \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"idea":"…"}'

Prices and jobs

Quote before, follow a job, change a finished one

POST/v1/tools/quotefree

Price work before it runs

Prices one call or a whole chain of calls before anything runs, with the workspace spending cap. Writes nothing and charges nothing. Where Scrub can tell, it also says about how big a file would come out.

FieldTypeWhat it is
stepsarrayThe calls, in the order they would run.
curl -X POST https://app.scrub.app/v1/tools/quote \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"steps":["w/…/file_id"]}'
POST/v1/tools/run_flowfree

Run a flow

Runs a flow by its name (flow_…) as calling that name does: one job at one price. For a flow saved in this conversation, which the tools listed before do not show. quote gives its price first, with the flow's name and its inputs.

FieldTypeWhat it is
flowstringIts name, flow_…
argumentsobjectIts inputs and settings, as open_flow gives them
curl -X POST https://app.scrub.app/v1/tools/run_flow \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"flow":"…","arguments":{}}'
POST/v1/tools/job_statusfree

Check a job

Where a job got to, and its result when it has one. Work that takes longer than a request can hold is handed back as a job id: ask this for it. While it runs: the stage, its count, and fraction_done, how much of the whole job is finished, with fraction_by: time when its steps are weighed by how long they usually take, steps when only counted; and seconds_left, about how long is left, where that can be told. For a tool’s run it also gives each step’s price and state, the files a finished step made, and the jobs made for the run before it started, with what they came to in all. Free.

FieldTypeWhat it is
job_idstring
curl -X POST https://app.scrub.app/v1/tools/job_status \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…"}'
POST/v1/tools/stop_jobfree

Stop a job

Stops a job that is still running, when the person asks to: it ends stopped and is not charged, and a tool’s finished steps are kept, so running it again reuses them. A job that has finished, or one answered at once, is left as it is. Free.

FieldTypeWhat it is
job_idstring
curl -X POST https://app.scrub.app/v1/tools/stop_job \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…"}'
POST/v1/tools/next_partfree

Ask the next part

What a tool's work offers next on these settings, free: the question of the part it is at and each choice with its price and the settings it runs with, as a result carries it in ask. For work done another way, such as on the tool's page. Only for tools made one part at a time.

FieldTypeWhat it is
toolstringThe tool
paramsobjectIts settings now
curl -X POST https://app.scrub.app/v1/tools/next_part \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool":"…","params":{}}'
POST/v1/tools/quote_adjustmentfree

Price a change

Prices a change to a finished job without running it. Free. Returns what adjust would charge for the same patch, and which params it changes. Adjustable params by action: burn_captions: start, end, position, text, captions; compress_video: quality; convert: as, by; make_spreadsheet: columns, sheet; read_fields: fields, row; transcribe: language; transcribe_audio: language.

FieldTypeWhat it is
job_idstring
patcharrayAn RFC 6902 JSON Patch against the job's params, e.g. [{"op":"replace","path":"/start","value":15.3}]. replace, add and test are accepted.
curl -X POST https://app.scrub.app/v1/tools/quote_adjustment \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…","patch":["w/…/file_id"]}'
POST/v1/tools/adjustpriced

Change a finished job

Re-runs a finished job with a patch applied, as a new job linked to the original, which is kept as it was. Charged the price quote_adjustment returns for the same patch. A chain of adjustments stops at 10. Adjustable params by action: burn_captions: start, end, position, text, captions; compress_video: quality; convert: as, by; make_spreadsheet: columns, sheet; read_fields: fields, row; transcribe: language; transcribe_audio: language.

FieldTypeWhat it is
job_idstring
patcharrayAn RFC 6902 JSON Patch against the job's params, e.g. [{"op":"replace","path":"/start","value":15.3}]. replace, add and test are accepted.
nonce optionalstringOptional. Send the same nonce again to get the same job back rather than a second charge.
curl -X POST https://app.scrub.app/v1/tools/adjust \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…","patch":["w/…/file_id"]}'
POST/v1/tools/download_filefree

Download a file

A link to download one of this workspace's files, an upload or a result, named as the person knows it. It works for an hour. Free.

FieldTypeWhat it is
file_idstringFrom list_files
curl -X POST https://app.scrub.app/v1/tools/download_file \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"w/…/file_id"}'
POST/v1/tools/profile_statusfree

About you

What this workspace said about itself after signing up: work or personal, the first thing they wanted done, what describes them, team size, company, how they heard of Scrub, and whether they want product news. Each may be unanswered. Free.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/profile_status \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/set_profilefree

Change about you

Changes what this workspace says about itself: any of the answers below, each left as it is when not given, cleared with an empty string. Product news is a yes only when the person said yes to its words. Free.

FieldTypeWhat it is
use optionalwork | personal | Is Scrub for work or personal use? work (Work), personal (Personal)
first_job optionalvideos | recordings | documents | building | other | What do you want to get done first? videos (Videos and voiceovers), recordings (Recordings into text), documents (Receipts, invoices and documents), building (Building with AI), other (Something else)
first_job_other optionalstringWith first_job other: what, in a few words.
role optionalfreelancer | small_business | creator | developer | bookkeeping | other | What describes you best? freelancer (Freelancer or agency), small_business (Small business), creator (Creator or podcaster), developer (Developer or AI builder), bookkeeping (Bookkeeping or admin), other (Something else)
role_other optionalstringWith role other: what they do, in a few words.
team optional1 | 2-10 | 11-50 | 51+ | How big is your team? Work only. 1 (Just me), 2-10 (2–10), 11-50 (11–50), 51+ (51+)
company optionalstringWhat is your company called? Work only.
heard optionalai | friend | search | social | media | other | How did you hear about Scrub? ai (My AI suggested it), friend (A friend or colleague), search (Search), social (Social media), media (A newsletter, podcast or video), other (Something else)
product_news optionalbooleanWhether they want product news by email. true only when the person says yes to these words: "Send me product news. Now and then. Unsubscribe from any email."
questions optionalshown | answered | skippedWhere the questions after sign-up stand: shown, answered, or skipped. Once answered or skipped they are not asked again.
curl -X POST https://app.scrub.app/v1/tools/set_profile \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/plan_statusfree

Plan status

This workspace's plan: Free with its balance, or a paid plan with how much of this month's work is left, when it renews, and whether additional usage is on. With the plans offered. Every price is the same on a plan; a paid plan buys a month of work for less. Free to call.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/plan_status \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/set_additional_usagefree

Set additional usage

On a plan: whether the workspace's credit pays once the month's work is used up (on), or nothing more is spent until it renews (off). Off by default. Free; turning it on waits for the person's yes.

FieldTypeWhat it is
onbooleantrue to let credit pay past the month.
curl -X POST https://app.scrub.app/v1/tools/set_additional_usage \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"on":true}'
POST/v1/tools/choose_planfree

Choose a plan

A Stripe page where the person starts a paid plan, or changes to the other one: it starts at once, and the month already paid is kept until it runs out. Give them the link; the plan starts when they pay. Free is every workspace without a paid plan, never bought: ending a plan on manage_plan returns to it. Free to ask; the plan's price is on the page.

FieldTypeWhat it is
planpro | maxpro: Pro, $20 a month for $25 of work; max: Max, $100 a month for $140 of work
curl -X POST https://app.scrub.app/v1/tools/choose_plan \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan":"pro"}'
POST/v1/tools/add_creditfree

Add credit

A Stripe page where the person adds credit to pay as they go, as Account's Add credit does: $20, $50, $100, or any whole amount from $5 to $500. Credit never expires; on a plan, a top-up buys more work at the plan's rate. Give them the link; the credit lands when they pay. Free to ask.

FieldTypeWhat it is
usdintegerHow much, in US dollars
curl -X POST https://app.scrub.app/v1/tools/add_credit \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"usd":1}'
POST/v1/tools/manage_planfree

Manage the plan

Stripe's billing page for this workspace's plan: its card, its invoices, and ending it, which happens when the month paid for ends. Give the person the link. Free.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/manage_plan \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/send_feedbackfree

Send feedback

Sends the Scrub team a problem the person met, or an idea for Scrub, in their own words, as they ask. A problem: something broken, wrong or confusing; give job_id when it is about one job. Files: screenshots, images, or a screen recording of up to 30 seconds with their voice. Only when the person asks to send something. Free. Up to 20 a day.

FieldTypeWhat it is
kindproblem | ideaA problem, or an idea
subject optionalstringA short summary, in their words
saidstringWhat happened, or what they want, in their own words: what they did, expected and saw, or what they are trying to get done
job_id optionalstringThe job it is about, such as job_3f9a1c2b7d4e, when it is about one
links optionalarrayWeb addresses it is about
files optionalarrayScreenshots, images or a short clip, each as base64
context optionalstringWhat the page knew, for the founder only: where, the browser, errors
curl -X POST https://app.scrub.app/v1/tools/send_feedback \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"problem","said":"…"}'
POST/v1/tools/list_updatesfree

Product updates

What Scrub has made live, has in beta, is building, plans and is considering, newest moves first: each with its stage, its area, the words written for people in parts (summary, sections, a dated note at each step, a picture or video), what it ships to try, the tools and actions it is about by id, and whether this workspace follows it, asked for it, joined its beta, or has not seen it. seen: true marks them all seen. Free.

FieldTypeWhat it is
stage optionallive | beta | dev | planned | idea | no | retiredOnly this stage
seen optionalbooleanMark every update seen, as opening Product updates does
curl -X POST https://app.scrub.app/v1/tools/list_updates \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/follow_updatefree

Follow an update

Follows a product update, by its id as list_updates gives it: the person is emailed when it moves stage, and the founder sees that they want it. on: false stops following. Free.

FieldTypeWhat it is
updatestringThe update’s id, as list_updates gives it
on optionalbooleanfalse to stop following
curl -X POST https://app.scrub.app/v1/tools/follow_update \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"update":"…"}'
POST/v1/tools/join_betafree

Join a beta

Joins the beta of a product update that is In beta, by its id as list_updates gives it: what it ships runs for this workspace before everyone, in every place Scrub is used, and the workspace follows it, so it hears when it moves. The founder sees who joined. on: false leaves the beta. Free.

FieldTypeWhat it is
updatestringThe update’s id, as list_updates gives it
on optionalbooleanfalse to leave the beta
curl -X POST https://app.scrub.app/v1/tools/join_beta \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"update":"…"}'
POST/v1/tools/search_helpfree

Search Scrub’s help

Scrub’s own help: a guide for each part of Scrub (getting started, plans and credit, connecting an AI, what is charged when work stops, Flows) and an article for every Tool and Action, with its price. Answer a question about how Scrub works from these, and say which article it came from. Empty words list every article. Free.

FieldTypeWhat it is
q optionalstringWords to look for, or the person’s own question, such as refund or how do I get my money back
curl -X POST https://app.scrub.app/v1/tools/search_help \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/message_supportfree

Message the Scrub team

Sends the person's message to the founder of Scrub, who answers in person: live while answering, otherwise replies by email within a day. For what Scrub's help and its tools cannot answer, or when the person asks for a person. Give carried: what they asked Scrub before, and the job it is about, so they are not asked twice. Only with the person's yes. Free.

FieldTypeWhat it is
saidstringTheir message, in their words
carried optionalstringWhat went before: what they asked Scrub and what it said, and the job, such as job_3f9a1c2b7d4e
curl -X POST https://app.scrub.app/v1/tools/message_support \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"said":"…"}'
POST/v1/tools/support_conversationfree

Your conversation with Scrub

The person's conversation with the founder of Scrub, newest last, whether the founder is answering now, and how many replies they have not seen. Reading it marks the replies seen. Free.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/support_conversation \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/connect_stepsfree

Connect an AI to Scrub

How a person adds Scrub to an AI they use, step by step, with the address or command to paste. Free. Give ai for one AI's steps, or nothing for every one. The AIs: Claude (claude), Claude Code (claude-code), Another AI (other). For any other AI, give other. asks are things to ask once it is connected.

FieldTypeWhat it is
ai optionalclaude | claude-code | otherWhich AI
curl -X POST https://app.scrub.app/v1/tools/connect_steps \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/list_connectionsfree

List connected AIs

The AIs connected to this workspace and not disconnected, newest first: each with its name, where it connected from, since when and when it was last used. Free. A person disconnects one in Account.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/list_connections \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/list_computersfree

List computers

The computers running the Scrub app for this workspace, the one seen last first: each with its own name, the app and its version, whether it is open to take work now, when it was last seen and since when, and the files the person gave Scrub there (the latest 20), by name, size and length. Free. Give a file's file_id (cf_…) to an action that runs on a computer to run it there, while the app is open; send it to Files with add_computer_file for anything else. A run on a computer is listed in Jobs like any job, under the computer's name.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/list_computers \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/remove_computerfree

Remove a computer

Removes a computer from this workspace, free: the Scrub app on it is signed out and records no more work there. Its jobs stay. Signing in again adds it back. Ask the person first.

FieldTypeWhat it is
computer_idstringpc_…, from list_computers
curl -X POST https://app.scrub.app/v1/tools/remove_computer \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"computer_id":"…"}'
POST/v1/tools/add_computer_filefree

Add a file from a computer to Files

Sends a file the person gave Scrub on a computer (cf_…, from list_computers) to Files, from the Scrub app there, while it is open. Free. Answers its file_id when it has arrived; a large file may still be on its way, so check list_computers, where it shows in Files once there. A file sent before and unchanged is not sent again. Then give the file_id to any tool.

FieldTypeWhat it is
file_idstringcf_…, from list_computers
curl -X POST https://app.scrub.app/v1/tools/add_computer_file \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"…"}'

Files

Put files in, find them, look at them, delete them

POST/v1/tools/create_uploadfree

Upload a file

Gets a place for one file and returns its file_id. upload_page is a link where a person picks the file: give it to them, and when they say it is uploaded, check with upload_status. An AI that can send bytes passes bytes and content_type to get upload_url as well. Files up to 4096 MB, larger than chat attachments. An upload no tool uses goes after a day; with keep it stays in the person's Files for 90 days, as any file does. Links last an hour. Free.

FieldTypeWhat it is
filename optionalstringThe name of the file, e.g. interview.mp4, if you know it
content_type optionalstringIts media type, e.g. video/mp4. Only needed with bytes.
bytes optionalnumberIts size in bytes. Only if you will send the bytes yourself.
keep optionalbooleanKeep it in Files for 90 days even if no tool uses it, for a person putting a file in Files. Off by default.
curl -X POST https://app.scrub.app/v1/tools/create_upload \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/upload_statusfree

Check an upload

Checks whether a file has arrived at a file_id from create_upload, and for a photo, scan or PDF how many pages it has. Free. Use it after the person says the upload finished, before passing the file_id to a tool.

FieldTypeWhat it is
file_idstring
curl -X POST https://app.scrub.app/v1/tools/upload_status \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"w/…/file_id"}'
POST/v1/tools/list_filesfree

List your files

Lists the files in this workspace, newest first, fifty at a time: uploads and results, each with its file_id to pass as a file input, its size, and what made it (with its job_id, and the results of that job that fill a setting, by type: a voice’s script). Free. Give tool (and input) to list only the files that input takes, kind to list only images, clips, sound or documents, name to find files whose name has those words, and file_ids to include those files however old. total says how many match; when more is true, pass the last file_id as before for the next fifty.

FieldTypeWhat it is
tool optionalstringAn action or tool id, to list only files it takes
input optionalstringOne of its file inputs, by name
kind optionalarrayOnly these kinds of file: image, video, audio, or document (anything else: PDFs, spreadsheets, text, subtitles, transcripts)
file_ids optionalarrayFiles to include however old, by file_id
name optionalstringOnly files whose name has these words, however old
before optionalstringThe next page: the files after this file_id, the last one listed
curl -X POST https://app.scrub.app/v1/tools/list_files \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/look_at_filesfree

Look at files

A quick look at a stack of files before reading them: what kind of document each is, how many pages, and the columns worth reading. Free. For Read documents into a table (read_fields). Use it to offer what to read before a run.

FieldTypeWhat it is
actionread_fields
file_idsarray
curl -X POST https://app.scrub.app/v1/tools/look_at_files \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action":"read_fields","file_ids":["w/…/file_id"]}'
POST/v1/tools/delete_filefree

Delete a file

Deletes a file from this workspace straight away, freeing its room; identical copies go with it. The jobs and receipts stay. It cannot be undone, so ask the person first. A file a running job is reading is not deleted.

FieldTypeWhat it is
file_idstring
curl -X POST https://app.scrub.app/v1/tools/delete_file \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"w/…/file_id"}'

Corrections

Correct a result by hand, free

POST/v1/tools/correct_transcriptfree

Correct a transcript

Saves a corrected copy of a finished transcription's words, free: one string per paragraph, in order, as the person wants them read. Words kept keep their times; the original stays; captions and conversions made afterwards use the corrected words.

FieldTypeWhat it is
job_idstringThe transcription job, or a tool run that made one
paragraphsarray
curl -X POST https://app.scrub.app/v1/tools/correct_transcript \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…","paragraphs":["w/…/file_id"]}'
POST/v1/tools/correct_tablefree

Correct a table

Corrects cells of a finished table, or adds a column read from its documents, free. Cells by row (from 0) and column key; the files made from the table, a spreadsheet made from it too, are written again with the corrections. The table as first made is kept.

FieldTypeWhat it is
job_idstring
cells optionalarray
add optionalarrayColumns to add, by name
curl -X POST https://app.scrub.app/v1/tools/correct_table \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"…"}'

Drafts

What is set on a page and not yet run, kept as it is made

POST/v1/tools/open_draftfree

Open a draft

A draft, free: what was set on a tool's or action's page and not yet run, kept by Scrub as it was changed, to carry on where a person or an AI left off. Open it by its name (draft_…, from list_drafts or a save), or by tool and from, the job it was started from (a result's next step, or a run being changed); by tool alone, the person's latest draft of it. Gives its name, tool, title, params as the page holds them, the job it came from, when it was saved and its version. placed gives each timeline as it plays, in seconds: its length; its safe area in each aspect ratio (its top left corner, width and height as shares of the frame, null for none), where text boxes, logos and captions belong; each scene with its start, end and words as [word, start, end]; each text box with when it shows and where it is drawn in each aspect ratio (its middle, width and height as shares of the frame); each pause with the word it follows, where it begins, how long it was voiced and how long it plays; and each caption with its first and last word, when it shows, the words said, what it shows instead when set (shows), and whether it was set by hand.

FieldTypeWhat it is
draft optionalstringIts name, draft_…
tool optionalstringThe tool or action
from optionalstringThe job it was started from, if any
curl -X POST https://app.scrub.app/v1/tools/open_draft \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/list_draftsfree

List drafts

A person's drafts, newest first, fifty at a time, free: what was set on a page and not yet run. Each with its name (draft_…), its tool, its own title (a script's title, the first words of an idea or a prompt, or the file it takes), when it was saved and the job it was started from. Give tool to list one tool's, words to find drafts with those words in them. total says how many match; when more is true, pass the last one's name as after for the next fifty.

FieldTypeWhat it is
tool optionalstringOnly this tool’s or action’s drafts
words optionalstringOnly drafts with these words in them
after optionalstringThe next page: the drafts after this one
curl -X POST https://app.scrub.app/v1/tools/list_drafts \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/save_draftfree

Save a draft

Keeps what is set on a tool's or action's page before it runs, free, so the person opens it there as it was left, on any device: its params by name, each as text, a script as its JSON as open_draft gives it (a script that is not one is refused). Nothing runs. Give draft, the name a save or open_draft gave, to save over that draft; or from, the job it was started from, for that job's one draft; give neither to start a new draft, and its name is given back. Give the version you opened to save over only that one: if it was saved since, nothing is saved and the newer one's time is given. Empty params delete it. A person keeps up to 500 drafts.

FieldTypeWhat it is
toolstringThe tool or action
draft optionalstringIts name, draft_…, to save over it
from optionalstringThe job it was started from, if any
paramsobject
version optionalinteger
curl -X POST https://app.scrub.app/v1/tools/save_draft \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool":"…","params":{}}'
POST/v1/tools/edit_draftfree

Change part of a draft

Changes part of a draft, free, without sending it all back: one pause, one caption line, one image. patch is JSON Patch (RFC 6902) on the draft's params by name: ops add, remove, replace and test, each {op, path, value}, applied in order, and a path goes into a param holding JSON, such as a script: /timeline/pauses/longest, /timeline/captions/lines/-, /timeline/scenes/2/line. Nothing runs. Give the version you opened to change only that one: if it was saved since, nothing changes and the newer one's time is given. If an op cannot be applied, a test does not hold, or a script no longer holds together, nothing changes and you are told why. placed gives each timeline as it plays, in seconds: its length; its safe area in each aspect ratio (its top left corner, width and height as shares of the frame, null for none), where text boxes, logos and captions belong; each scene with its start, end and words as [word, start, end]; each text box with when it shows and where it is drawn in each aspect ratio (its middle, width and height as shares of the frame); each pause with the word it follows, where it begins, how long it was voiced and how long it plays; and each caption with its first and last word, when it shows, the words said, what it shows instead when set (shows), and whether it was set by hand.

FieldTypeWhat it is
draftstringIts name, draft_…
patcharrayThe changes, in order
version optionalinteger
curl -X POST https://app.scrub.app/v1/tools/edit_draft \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"draft":"…","patch":["w/…/file_id"]}'
POST/v1/tools/draft_historyfree

See a draft’s history

A draft's history, free: every version it was saved at, newest first, fifty at a time, each with when it was saved, who saved it (by: a tool page, Chat, the assistant, a connected AI by its name, an API key, a computer; none where it was not recorded), where (page, chat, assistant, mcp, api or computer) and how many changes made it from the version before. Give version to open that one as well: its params as open_draft gives them, and patch, its change from the version before as JSON Patch on the params. To go back to an earlier version, save its params over the draft with save_draft. total says how many versions there are; when more is true, pass the last one's version as before for the next fifty. placed gives each timeline as it plays, in seconds: its length; its safe area in each aspect ratio (its top left corner, width and height as shares of the frame, null for none), where text boxes, logos and captions belong; each scene with its start, end and words as [word, start, end]; each text box with when it shows and where it is drawn in each aspect ratio (its middle, width and height as shares of the frame); each pause with the word it follows, where it begins, how long it was voiced and how long it plays; and each caption with its first and last word, when it shows, the words said, what it shows instead when set (shows), and whether it was set by hand.

FieldTypeWhat it is
draftstringIts name, draft_…
version optionalintegerA version to open
before optionalintegerThe next page: the versions before this one
curl -X POST https://app.scrub.app/v1/tools/draft_history \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"draft":"…"}'
POST/v1/tools/delete_draftfree

Delete a draft

Deletes a draft for good, by its name (draft_…): what was set on a page and not yet run. The runs made from it stay. It cannot be undone, so ask the person first.

FieldTypeWhat it is
draftstringIts name, draft_…
curl -X POST https://app.scrub.app/v1/tools/delete_draft \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"draft":"…"}'

Flows

Tools and actions chained on the canvas, kept with each version. A flow runs at /v1/tools/<its name>, as a tool does.

POST/v1/tools/list_flowsfree

List flows

This workspace's flows, newest saved first, free: what a person or an AI chained on the canvas from tools and actions. Each with its name (flow_…, the name it runs by, as any tool does), its title, its version, when it was saved and its steps by their names.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/list_flows \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/open_flowfree

Open a flow

A flow, free, by its name (flow_…): its definition as save_flow takes it (its inputs, its settings and its steps, each running an action or one of Scrub's tools on the flow's inputs or an earlier step's result) and where its steps sit on the canvas. Its latest version, or an earlier one by version.

FieldTypeWhat it is
flowstringIts name, flow_…
version optionalintegerAn earlier version
curl -X POST https://app.scrub.app/v1/tools/open_flow \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"flow":"…"}'
POST/v1/tools/save_flowfree

Save a flow

Saves a flow, free: its title, its definition as open_flow gives it (inputs, params and steps; at most 20 steps), and where its steps sit. A step may be one of Scrub's tools ({id, tool, inputs, params, pass}): it runs as that tool is when the flow runs, its settings fixed by params, given each run by pass, or the tool's own. It is checked as a tool is: every step's inputs from the flow's inputs or an earlier step's result, every step priced by what is given before anything runs; one that would not run is not saved, and says why. A new flow is given its name (flow_…) and runs by that name, as any tool does, priced before it runs; its title changes freely. Saving one by its name keeps a new version; given version, it is saved only if that is still its latest.

FieldTypeWhat it is
flow optionalstringIts name, flow_…, to save a new version
titlestringWhat it is called
definitionobjectIts inputs, params and steps, as open_flow gives them; its title, version, category and icon are filled in from its title and first step when left out.
layout optionalobjectWhere its steps sit on the canvas
version optionalintegerThe version it was opened at
curl -X POST https://app.scrub.app/v1/tools/save_flow \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"…","definition":{}}'
POST/v1/tools/delete_flowfree

Delete a flow

Deletes a flow for good, by its name (flow_…): it no longer runs. Its runs stay. It cannot be undone, so ask the person first.

FieldTypeWhat it is
flowstringIts name, flow_…
curl -X POST https://app.scrub.app/v1/tools/delete_flow \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"flow":"…"}'

Brand Kits

A workspace's look, kept by name: a timeline's own parts, put on a video by writing them into its timeline.

POST/v1/tools/list_brand_kitsfree

List Brand Kits

This workspace's Brand Kits, by name, free: the look a person keeps for their videos. Each with its id (kit_…), its name and its parts, which are a timeline's own: brand {back, text, accent, font}, captions, safe, voice, music {prompt}, and logo {src, box?}, an image shown from the first scene to the last. To put a kit on a video, write its parts into the video's timeline: brand, captions and safe over the video's own, voice as given, music only where the video has none made, and the logo as an image with id kit-logo from the first scene's start until the last scene's end.

No fields: send {}.

curl -X POST https://app.scrub.app/v1/tools/list_brand_kits \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
POST/v1/tools/save_brand_kitfree

Save a Brand Kit

Saves a Brand Kit, free: its name and its parts, each as a timeline has it (brand {back, text, accent, font}, captions, safe, voice, music {prompt}, logo {src: a file_id, box?}), checked as a timeline checks them; one that is not right is not saved, and says why. A new kit is given its id (kit_…); saving one by its id saves over it. A workspace keeps at most 50.

FieldTypeWhat it is
kit optionalstringIts id, kit_…, to save over it
namestringWhat it is called
partsobjectIts parts, as a timeline has them
curl -X POST https://app.scrub.app/v1/tools/save_brand_kit \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"…","parts":{}}'
POST/v1/tools/delete_brand_kitfree

Delete a Brand Kit

Deletes a Brand Kit for good, by its id (kit_…). Videos made with it keep their look. It cannot be undone, so ask the person first.

FieldTypeWhat it is
kitstringIts id, kit_…
curl -X POST https://app.scrub.app/v1/tools/delete_brand_kit \
  -H "Authorization: Bearer $SCRUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kit":"…"}'