Reference
render_strip
Raw app screenshots in, finished App Store panels out — the call that produces the pixels.
What it does
- Writes
- No account needed
The main event. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store strip with the headlines you pass, and returned as one PNG per panel at the exact store panel size. Nothing is saved and no project is created; this call only renders, and the result says so — a render with no project carries an offer to save, not an instruction to. Start with a preview — it costs a quarter of a full-resolution render and is what you iterate against — and switch to full resolution once the composition is right.
You can ask: “Turn these imported Tidebook screenshots into a three-panel preview.”
The published schema deliberately accepts any shape inside screenshots so a malformed entry comes back as a sentence rather than a stacked union error. The real element shapes are below under what the server accepts.
Input
screenshotsarrayrequiredat least 1 itemat most 10 itemsScreenshots in App Store display order (1–10). Preferred common form: a FLAT array, where each entry becomes its own App Store panel, e.g. [{ "ref": "first" }, { "ref": "second" }]. Advanced multi-phone form: a NESTED array of slots, where several entries inside one slot are composited into that single panel, e.g. [[{ "ref": "left" }, { "ref": "right" }], [{ "ref": "third" }]]. Each screenshot entry is EITHER an inline base64 PNG string (capped at 3MB of image data — a full-resolution screenshot is 3-4MB and will NOT fit; use a ref for anything real), OR { "ref": "..." } from request_screenshot_upload, OR { "url": "https://..." }, OR (local stdio mode only) { "path": "/abs/or/relative/path.png" } to read a file straight off disk, OR { "locales": { "<locale>": <any of those> } } to vary that screenshot per App Store locale (missing locales fall back to the en-US variant). Prefer ref/url for real screenshots so the bytes never transit this conversation.
each item
any
The published schema does not constrain this element, on purpose — a malformed entry comes back as a sentence instead of a stacked union error. What the server re-validates against is below.
panelPresetIdstringoptionalApp Store screenshot size. Default r69 (6.9″ iPhone, 1290×2796). iPhone sizes only — iPad is not rendered yet.
one of
r69r65r55clipstringoptionalAn explicit top-level
clip: "strip"orclip: "panel"wins over every shot;"strip"forces continuous overflow and"panel"clips every device to its own panel. Omitclipto honour each shot'sclipToFrame; when it is missing or false, overflow is allowed.one of
strippanellookobjectoptionalA ShotOps "look" — per-DEVICE styling + background, exactly as read_look returns it. Its shots[] give every screenshot/phone its own styling (including multiple phones in one panel); style.shotLook is one shared style for all. COMPOSES with
style: pass a look for the devices +style.captionsfor the words in ONE render. Validated — unknown keys are rejected (call describe_look for the full field catalog).schemanumberoptionalLook schema version. The server stamps it on every save (save_look); read_look returns it. Any input value is ignored — never set it yourself.
panelPresetIdstringoptionalCanvas size preset, e.g. r69 = 1290×2796. Default r69.
bgModestringoptionalOne gradient across the strip, or a flat colour per panel. Default gradient.
one of
gradientperPanelgradientStopsarrayoptionalat least 1 itemCanonical gradient stops — { color (hex), at (0–1 position) } — wins over gradientFrom/gradientTo/gradientDir when present. Read this back for the full stop list a Studio-authored look may carry; the trio below is only ever a two-stop summary.
each item
object
colorstringrequiredatnumberrequiredmin 0max 1
gradientAnglenumberoptionalCanonical gradient angle in degrees, clockwise from "to top" (CSS convention: 90 = right, 180 = down, the gradientDir:"vertical" equivalent). Paired with gradientStops.
gradientFromstringoptionalGradient start (hex) — a DERIVED mirror of gradientStops[0], re-derived from the stops on every render and reload. Setting it ALONE changes nothing: set gradientStops to change the backdrop. Default #1b1b2e.
gradientTostringoptionalGradient end (hex) — a DERIVED mirror of the last gradientStops entry, re-derived from the stops on every render and reload. Setting it ALONE changes nothing: set gradientStops to change the backdrop. Default #0a0a14.
gradientDirstringoptionalA DERIVED mirror of gradientAngle (nearest axis), re-derived on every render and reload. Setting it ALONE changes nothing: set gradientAngle to turn the gradient. Default vertical.
one of
horizontalverticalpanelColorsobjectoptionalPer-panel flat colours (mode perPanel), keyed by panel id.
shadowbooleanoptionalDrop shadow under the phones. Default true.
floorReflectionbooleanoptionalFlipped, faded floor reflection under each phone. Default false.
panelBackgroundsobjectoptionalPer-frame background overrides keyed by panel id (web-authored; agents rarely set these).
captionStylesobjectoptionalPer-panel caption STYLE arrays keyed by panel id (styling only, no words — for caption text pass style.captions).
shotsarrayoptionalat most 60 itemsOne entry per SCREENSHOT/DEVICE, flattened in panel order. Each entry has its OWN
look. For screenshots:[[left,right]], pass two shots with the SAME panelId ("panel-1") to give the two devices different colours/materials/angles. This is how a look varies devices; style.shotLook cannot.each item
object
panelIdstringoptionalPanel containing this screenshot/device. Panels are panel-1…panel-N in slot order; REPEAT the same panelId for multiple devices in one panel. A saved look’s own ids are remapped by ordinal.
lookobjectoptionalThis individual screenshot/device’s styling — the SAME fields as style.shotLook.
anglestringoptionalCamera preset. Default front.
one of
frontleftrightcameraPosone of 2 shapesoptionalManual camera position override; null/omit = use the angle preset.
1
xxnumberrequiredynumberrequiredznumberrequired
2
nullrollstring or numberoptional-45–45° clock-hand tilt. Default 0.
phoneHeightstring or numberoptional-45–45° clock-hand tilt. Default 0.
hOffsetstring or numberoptional-45–45° clock-hand tilt. Default 0.
vOffsetstring or numberoptional-45–45° clock-hand tilt. Default 0.
materialstringoptionalDevice body. Default real.
one of
realclaycolorwaystringoptionalBody colour (material "real"). Default silver. These are finish SLOTS — each device binds its own real finish (silver = iPhone Silver / Pixel Moonstone). "green" is Pixel-only (Jade); other devices fall back to their first finish.
one of
orangebluesilvergreencustomcustomColorstringoptionalHex body colour when colorway is "custom".
finishstring or numberoptional-45–45° clock-hand tilt. Default 0.
clearcoatstring or numberoptional-45–45° clock-hand tilt. Default 0.
clayTonestringoptionalClay tone (material "clay"). Default grey.
one of
greywhitecharcoalcustomclayCustomstringoptionalHex clay colour when clayTone is "custom".
flatScreenbooleanoptionalRender the screen flat (no curvature). Default false.
glarebooleanoptionalScreen glare. Default false.
lightingbooleanoptionalScene lighting. Default true.
reflectionsbooleanoptionalBody reflections. Default false.
clipToFramebooleanoptionalClip this complete device composite to its owning frame. Default false (overflow allowed).
styleobjectoptionalStructured styling — the discoverable path (call describe_look for the full field catalog + defaults).
layoutis the fastest way in: one named composition that settles the headline region and the device placement together, with your explicit fields still winning over it. COMPOSES withlook: pass BOTH to get per-device styling from thelookAND captions fromstyle.captionsin ONE render (every real App Store strip). When alookis also given it supplies the devices + background, sostyle.shotLook/style.backgroundare ignored (a note says so) — usestyleforcaptionsthen. Alone,stylestyles every phone identically + captions. Also composes withuseSavedLook/version.layoutstringoptionalPanel COMPOSITION template — sets the headline’s reserved region and the device’s placement, camera pose, and roll in one choice. "standard" — Headline on top, whole device below it, nothing cropped. The safe default. Reserves 2 headline lines (~36 characters). Character: [safe]. "bleed" — Room for a longer headline, device running off the bottom edge. What the market looks like. Reserves 4 headline lines (~72 characters). Character: [conventional]. "top-bleed" — Device running off the TOP edge, headline underneath it. Inverts the usual reading order. Reserves 4 headline lines (~72 characters). Character: [expressive]. PRECEDENCE: the template expands FIRST into shotLook.phoneHeight/shotLook.hOffset/shotLook.vOffset/shotLook.angle/shotLook.cameraPos/shotLook.roll and captions[].sizePt/captions[].maxWidth/captions[].reserveLines/captions[].band, and THEN any of those fields you pass EXPLICITLY overrides it — so { layout: "bleed", shotLook: { vOffset: "45" } } renders bleed seated at 45, not at the template’s 38. That is how you say "bleed, but a bit lower" without the template silently winning. The reserved region is held at FULL SIZE whether or not the headline fills it, which is what makes every panel in a swiped set land on the same line. Nothing stores the template id: it is discarded at expansion, so a saved project and read_look carry only the expanded values.
one of
standardbleedtop-bleedshotLookobjectoptionalThis individual screenshot/device’s styling — the SAME fields as style.shotLook.
anglestringoptionalCamera preset. Default front.
one of
frontleftrightcameraPosone of 2 shapesoptionalManual camera position override; null/omit = use the angle preset.
1
xxnumberrequiredynumberrequiredznumberrequired
2
nullrollstring or numberoptional-45–45° clock-hand tilt. Default 0.
phoneHeightstring or numberoptional-45–45° clock-hand tilt. Default 0.
hOffsetstring or numberoptional-45–45° clock-hand tilt. Default 0.
vOffsetstring or numberoptional-45–45° clock-hand tilt. Default 0.
materialstringoptionalDevice body. Default real.
one of
realclaycolorwaystringoptionalBody colour (material "real"). Default silver. These are finish SLOTS — each device binds its own real finish (silver = iPhone Silver / Pixel Moonstone). "green" is Pixel-only (Jade); other devices fall back to their first finish.
one of
orangebluesilvergreencustomcustomColorstringoptionalHex body colour when colorway is "custom".
finishstring or numberoptional-45–45° clock-hand tilt. Default 0.
clearcoatstring or numberoptional-45–45° clock-hand tilt. Default 0.
clayTonestringoptionalClay tone (material "clay"). Default grey.
one of
greywhitecharcoalcustomclayCustomstringoptionalHex clay colour when clayTone is "custom".
flatScreenbooleanoptionalRender the screen flat (no curvature). Default false.
glarebooleanoptionalScreen glare. Default false.
lightingbooleanoptionalScene lighting. Default true.
reflectionsbooleanoptionalBody reflections. Default false.
clipToFramebooleanoptionalClip this complete device composite to its owning frame. Default false (overflow allowed).
backgroundobjectoptionalmodestringoptionalOne gradient across the strip, or a flat colour per panel.
one of
gradientperPanelstopsarrayoptionalat least 1 itemGradient colour stops, in order along the gradient line — { color (hex), at (0–1 position; 0 = line start, 1 = line end) }. Two stops is the classic top/bottom gradient, more make a multi-colour sweep, one stop is a solid fill. Default a two-stop navy gradient: [{color:"#1b1b2e",at:0},{color:"#0a0a14",at:1}].
each item
object
colorstringrequiredatnumberrequiredmin 0max 1
anglenumberoptionalGradient angle in degrees, clockwise from "to top" — CSS
linear-gradient()convention: 90 = to right, 180 = to bottom (the old "vertical" default, and this field’s own default), 270 = to left. Any value folds into 0–360.panelColorsarrayoptionalat most 10 itemsPer-panel flat colours (mode "perPanel"), one entry per slot in order; null = default.
each item
string or null
shadowbooleanoptionalDrop shadow under the phones. Default true.
floorReflectionbooleanoptionalFlipped, faded floor reflection under each phone. Default false.
captionsarrayoptionalat most 10 itemsOne entry PER PANEL in slot order; null = no caption on that panel. An entry is EITHER a single caption object OR an array of caption layers (stacked, in order) on that panel. Alongside a
look, an entry is a DELTA: every style field you set wins, every field you leave out keeps the look’s styling for that panel and layer — which is what lets you re-render alayout.correctedLookwithout restating your captions.each item
one of 2 shapes
1
one of 2 shapes1
objectfontIdstringoptionalBundled font. Default inter.
one of
intermanropepoppinsfrauncesspace-grotesksizePtnumberoptionalgreater than 0Font size in iOS points (preset-independent), 20–120. Default 32. Outside that range it is clamped to it and the response says so.
colorstringoptionalText colour (hex). Default FFFFFF.
alignstringoptionalDefault center.
one of
leftcenterrightanchorobjectoptionalNormalized 0–1 position of the caption on the panel. Default {x:0.5, y:0.06}.
xnumberoptionalmin 0max 1ynumberoptionalmin 0max 1
maxWidthnumberoptionalgreater than 0Wrap width as a fraction of the panel, 0.1–1. Default 0.86. Outside that range it is clamped to it and the response says so.
reserveLinesintegeroptionalmin 1max 8How many lines of headline room to HOLD, whether or not the text fills it. A shorter headline is centred in the reserved space rather than leaving all the slack beneath it, and the space is held so every panel in the strip lands on the same line. Omit to let the caption be exactly as tall as its text. Set for you by style.layout.
bandstringoptionalPut this caption in the BOTTOM band, growing upward from the lower margin, instead of the default top band. This is how a headline sits UNDER the device (see the top-bleed layout). The caption stays auto-placed — it still reflows and still stacks; use this rather than pinning an anchor, which would freeze it.
one of
bottombandInsetnumberoptionalmin 0max 0.5Inset of the auto caption band from its own panel edge, as a 0–0.5 fraction of panel height. The top band measures down; the bottom band mirrors it upward. Default 0.06.
lineHeightnumberoptionalmin 0.5max 3Line spacing as a multiple of font size, 0.5–3. Default 1.2.
bandCenternumberoptionalmin 0max 1Horizontal reference of the auto caption block as a 0–1 fraction of panel width. Text alignment decides which edge sits there. Default 0.5.
textstringoptionalat most 200 charactersThe headline — per-RENDER input, never stored in a look.
subtitlestringoptionalat most 300 charactersOptional subtitle under the headline — also per-render input.
2
arrayeach item
object
fontIdstringoptionalBundled font. Default inter.
one of
intermanropepoppinsfrauncesspace-grotesksizePtnumberoptionalgreater than 0Font size in iOS points (preset-independent), 20–120. Default 32. Outside that range it is clamped to it and the response says so.
colorstringoptionalText colour (hex). Default FFFFFF.
alignstringoptionalDefault center.
one of
leftcenterrightanchorobjectoptionalNormalized 0–1 position of the caption on the panel. Default {x:0.5, y:0.06}.
xnumberoptionalmin 0max 1ynumberoptionalmin 0max 1
maxWidthnumberoptionalgreater than 0Wrap width as a fraction of the panel, 0.1–1. Default 0.86. Outside that range it is clamped to it and the response says so.
reserveLinesintegeroptionalmin 1max 8How many lines of headline room to HOLD, whether or not the text fills it. A shorter headline is centred in the reserved space rather than leaving all the slack beneath it, and the space is held so every panel in the strip lands on the same line. Omit to let the caption be exactly as tall as its text. Set for you by style.layout.
bandstringoptionalPut this caption in the BOTTOM band, growing upward from the lower margin, instead of the default top band. This is how a headline sits UNDER the device (see the top-bleed layout). The caption stays auto-placed — it still reflows and still stacks; use this rather than pinning an anchor, which would freeze it.
one of
bottombandInsetnumberoptionalmin 0max 0.5Inset of the auto caption band from its own panel edge, as a 0–0.5 fraction of panel height. The top band measures down; the bottom band mirrors it upward. Default 0.06.
lineHeightnumberoptionalmin 0.5max 3Line spacing as a multiple of font size, 0.5–3. Default 1.2.
bandCenternumberoptionalmin 0max 1Horizontal reference of the auto caption block as a 0–1 fraction of panel width. Text alignment decides which edge sits there. Default 0.5.
textstringoptionalat most 200 charactersThe headline — per-RENDER input, never stored in a look.
subtitlestringoptionalat most 300 charactersOptional subtitle under the headline — also per-render input.
2
null
useSavedLookbooleanoptionalIf true, style the strip with the project's saved look (ignored when
look/styleis given).projectstringoptionalat least 1 characterWhich ShotOps project to target: its id (as returned by read_look/save_project), OR its NAME as the user says it — matched exactly, ignoring case and surrounding spaces. Omit = your most recently edited project. A name that matches none or several is refused with the account's projects listed, never resolved by guessing.
versionintegeroptionalmin 1Render a specific saved look version of the project (implies the saved look). Omit = the project's HELD version if one is held (hold_look), else the latest saved look.
localestringoptionalApp Store locale, e.g. "de-DE" (default en-US). Labels the render, selects which { "locales": … } screenshot variants render, and, for emit_bundle, selects the fastlane screenshots folder. Does not select caption text — pass the copy for this locale yourself via
style.captions[].text(see the README'scaptions.<locale>.jsonconvention).previewbooleanoptionalRender at ~25% resolution for a fast styling preview at a quarter the render cost — small enough to always come back inline. Not for final delivery: re-render without
preview(or use emit_bundle directly) once the look is right.outputstringoptionalHow to return rendered bytes: "inline" (base64), "urls" (uploaded, short-lived signed download URLs — use for real/full-resolution renders so bytes never transit this conversation), or omit for auto (inline under ~200KB total, urls above).
one of
inlineurlswaiversarrayoptionalApprovals for readiness findings this run would otherwise be blocked on. Pass back the exact
contract.readiness.findings[]id and digest from apreview: truerun, or from the refusal you just received. Each waiver clears ONE finding; fatal findings are never waivable.each item
object
findingIdstringrequiredThe
contract.readiness.findings[].idthis waiver authorizes. Exactly one.findingDigeststringrequiredThat finding’s
digest. A digest from an earlier run no longer matches and is rejected as stale.policyVersionstringrequiredcontract.readiness.policyVersionas it stood when the finding was raised.waivedBystringrequiredThe account that approved it. Must be the account making this call.
waivedAtstringrequiredISO 8601.
reasonstringoptionalFree text, for the receipt. It authorizes nothing on its own.
idempotencyKeystringoptionalat least 1 characterat most 200 charactersStable retry key for paid Hosted production. Reuse it only for the identical request; a new key deliberately requests a new render. Omit it for deterministic fingerprint retry safety.
What the server actually accepts
The published schema above deliberately accepts a wider shape for screenshots, so a malformed entry comes back as a sentence rather than a stack of union errors. The server re-validates against these shapes and refuses anything else.
screenshotsone of 2 shapesrequiredScreenshots in App Store display order (1–10). Preferred common form: a FLAT array, where each entry becomes its own App Store panel, e.g. [{ "ref": "first" }, { "ref": "second" }]. Advanced multi-phone form: a NESTED array of slots, where several entries inside one slot are composited into that single panel, e.g. [[{ "ref": "left" }, { "ref": "right" }], [{ "ref": "third" }]]. Each screenshot entry is EITHER an inline base64 PNG string (capped at 3MB of image data — a full-resolution screenshot is 3-4MB and will NOT fit; use a ref for anything real), OR { "ref": "..." } from request_screenshot_upload, OR { "url": "https://..." }, OR (local stdio mode only) { "path": "/abs/or/relative/path.png" } to read a file straight off disk, OR { "locales": { "<locale>": <any of those> } } to vary that screenshot per App Store locale (missing locales fall back to the en-US variant). Prefer ref/url for real screenshots so the bytes never transit this conversation.
1
arrayPreferred: one screenshot entry per App Store panel, in display order.
each item
one of 5 shapes
1
stringInline base64-encoded PNG (no "data:" prefix). Capped at 3MB of image data — a real full-resolution screenshot is 3-4MB and will NOT fit. Use request_screenshot_upload and pass { ref } instead; inline is for thumbnails and tests.
2
refA ref returned by request_screenshot_upload — resolved server-side, never re-sent inline.
refstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
3
urlAn https URL to a PNG or JPEG — fetched server-side, converted to PNG when needed (no redirects, ~20MB cap).
urlstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
4
pathA local filesystem path to a PNG, read straight off disk — ONLY available over the local stdio server (npx shotops-mcp); the hosted server rejects this entry shape.
pathstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
5
localesPer-locale variants of ONE screenshot: { "locales": { "en-US": <entry>, "de-DE": <entry> } } (each variant is an inline base64 / ref / url / path entry). render_strip picks the top-level
locale's variant; emit_bundle withlocalesrenders/packages every listed locale. A locale with no variant of its own falls back to the en-US variant (else the first declared), so you can localize only some screenshots.localesobjectrequired
2
arrayAdvanced: one nested slot per panel; put several screenshot entries in a slot for a multi-phone panel.
each item
array
One App Store slot: 1–6 screenshots. Several entries = several phones composited into that one slot.
each item
one of 5 shapes
1
stringInline base64-encoded PNG (no "data:" prefix). Capped at 3MB of image data — a real full-resolution screenshot is 3-4MB and will NOT fit. Use request_screenshot_upload and pass { ref } instead; inline is for thumbnails and tests.
2
refA ref returned by request_screenshot_upload — resolved server-side, never re-sent inline.
refstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
3
urlAn https URL to a PNG or JPEG — fetched server-side, converted to PNG when needed (no redirects, ~20MB cap).
urlstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
4
pathA local filesystem path to a PNG, read straight off disk — ONLY available over the local stdio server (npx shotops-mcp); the hosted server rejects this entry shape.
pathstringrequiredat least 1 characternamestringoptionalat most 200 charactersThe screenshot's original filename (e.g. "03_statistics.png"). Pass it so that, if this render is saved as a project, the user's own screenshots folder re-loads exactly this file by name. Optional if the ref already carries a name (from request_screenshot_upload({ names })); omit entirely and the record falls back to a positional name (screen-N.png).
5
localesPer-locale variants of ONE screenshot: { "locales": { "en-US": <entry>, "de-DE": <entry> } } (each variant is an inline base64 / ref / url / path entry). render_strip picks the top-level
locale's variant; emit_bundle withlocalesrenders/packages every listed locale. A locale with no variant of its own falls back to the en-US variant (else the first declared), so you can localize only some screenshots.localesobjectrequired
Result
okbooleanrequiredcountnumberoptionalpanelPresetIdstringoptionalpanelWidthnumberoptionalpanelHeightnumberoptionalrenderMsnumberoptionaloutputstringoptionalone of
inlineurlslocalestringoptionalnextStepobjectoptionalactionstringrequiredalways "offer_save_project"An offer to put to the user. Never a command, and never consent already given.
toolstringrequiredalways "save_project"The door that would persist this work, if they ask for it.
projectCreatedbooleanrequiredalways falseNo project was created or updated by this call — the same fact as
contract.effects.projectMutation.consentstringrequiredalways "required"Saving needs the user’s own preserve/reuse/keep-editing intent. A client that cannot ask returns this offer unresolved rather than saving.
reasonstringrequired
creditsobjectoptionalCredits this call spent from the account wallet, the balance left afterwards, and when that wallet is next topped up (ISO 8601; absent when no further refill is scheduled). Renders cost per panel; reads, saves, uploads and deletes are free, as is bundling panels that were already rendered.
debitednumberrequiredremainingnumberrequiredrefillAtstringoptional
notestringoptionalFree-text advisories about THIS call, joined into one string. Written when the server did something you did not literally ask for or could not have known: a look/version fallback, an output-mode downgrade, a device this server cannot render, a
style.shotLookalookoverrode, or placement set by hand where astyle.layouttemplate would have produced the same composition. Never an error — the call succeeded. Read it, and act on it next call.layoutobjectoptionalWhat is wrong with this render’s COMPOSITION, measured on the panels it just returned. ABSENT means there is nothing to report — a clean strip, or one whose captions could not be measured. Advisory: the panels are already rendered and nothing here changed a pixel.
findingsarrayrequiredeach item
object
kindstringrequiredWhat overlaps what. Captions must never overlap each other or a device; devices MAY overlap, and are only reported here when the pair does NOT read as composed (no real difference in scale, camera angle or roll).
one of
caption-captioncaption-devicedevice-deviceframesarrayrequired0-based indexes into the returned
panels. For a caption finding the FIRST entry is the frame whose caption is the one to change; a second appears only when the other party sits in a different frame.each item
number
depthPctnumberrequiredHow deep the overlap runs, in whole percent of panel height — or, for
device-device, the percentage of the smaller device the other one covers.anchoredbooleanrequiredtrue ⟹ a caption was placed by hand (an explicit
anchor), so nothing may rewrite it.clearAtScalenumberoptionalThe verified factor that clears this frame’s auto-placed captions — 0.82 means 82% of their current size. Absent when no size above the legibility floor clears it, or when the offending caption is a hand placement.
correctedLookanyoptionalThe look this render used, with every caption shrunk to the largest size verified to clear the overlap. Pass it back as
lookto re-render. Absent when no repair is deterministic — a device-device overlap is a taste call (give the pair a real difference in scale, angle or roll) and a hand-anchored caption is a human placement.notesays which.
contractobjectoptionalThe versioned ShotOps result contract (#662): what ran, where, what it changed, what it cost, what it produced and — on a refusal — a typed failure over a closed code catalog. Structured content is authoritative; the prose beside it is a rendering of this block.
contractVersionstringrequiredThe version of THIS envelope, REPORTED. Compare it against the version you were written for. There is no version negotiation: no tool accepts a requested version, so this is never a refusal — it moves only when a field changes meaning, and additive fields never move it.
statusstringrequiredone of
succeededpartialfailedrefusedacceptedqueuedrunningcancel_requestedcancelledterminalbooleanrequiredfalse ⟹ this operation is still running and will be reported again (#666).
operationobjectrequiredidstringrequiredIdentity for THIS call, unique per invocation. Quote it in a bug report.
toolstringrequiredThe registered tool name that produced this result.
kindstringrequiredone of
renderbundlereadmutateuploaddeletestatusdurableobjectoptional#666 domain handle for addressable long-running work. Absent from synchronous calls.
operationIdstringrequiredpollWithstringoptional
executionobjectrequiredlocationstringrequiredWhere the work RAN.
localis the caller’s own machine over stdio.one of
localhostedinputModesarrayrequiredEvery way this door accepts screenshots and assets.
each item
string
one of
inline_base64stored_refremote_urllocal_pathproject_storeddeliveryModesarrayrequiredEvery way this door can hand a result back.
each item
string
one of
inline_base64signed_urllocal_pathshare_link
effectsobjectrequiredWhat this call DID to the world, independent of what it returned. Every flag is stated on every result, false included: an absent flag would be indistinguishable from an effect nobody thought to declare.
networkFetchbooleanrequiredThis call fetched bytes from a host neither ShotOps nor the caller controls.
uploadbooleanrequiredCaller bytes were uploaded into ShotOps storage.
retainedStoragebooleanrequiredSomething survives this call in ShotOps storage.
projectMutationbooleanrequiredA saved project or its look history changed.
publicationbooleanrequiredSomething became reachable outside the account — a share link.
deletionbooleanrequiredSomething was permanently removed.
costobjectrequiredunitstringrequiredalways "credit"Public cloud credits — the same unit every ShotOps surface quotes.
modelstringrequiredHow this connection pays.
unmeteredis local stdio, which renders on the caller’s own machine;anonymous_allowanceis the unsigned hosted taste.one of
meteredanonymous_allowanceunmeteredestimatednumberoptionalWhat the call was expected to cost, before it ran.
reservednumberoptionalHeld against the wallet for the duration of the call (#666).
releasednumberoptionalGiven back — an unused reservation or a refund after a post-charge failure (#666).
settlednumberoptionalActually taken. Absent when nothing was charged.
balanceAfternumberoptionalThe wallet’s public cloud credit balance once this call settled.
refillAtstringoptionalISO 8601. When the wallet is next topped up; absent when none is scheduled.
progressobjectoptionalPersisted monotonic durable-operation progress. Never inferred from transient render metadata. Relay
panelsCompleted/panelsTotaland the estimate to the waiting user; the item counters are internal bookkeeping and do not match what they asked for.completedintegerrequiredmin 0Internal scheduling items done — panels PLUS the bundle and result items. Do not quote this to a person.
totalintegerrequiredmin 0Internal scheduling items in total. Larger than the panel count.
panelsCompletedintegeroptionalmin 0Panels finished, in the unit the caller asked in. THIS is the number to report.
panelsTotalintegeroptionalmin 0Panels this operation will produce — the count the user asked for.
attemptintegerrequiredmin 0heartbeatAtstring or nulloptionalestimatedRemainingSecondsnumberoptionalmin 0Rough seconds of rendering left, from this operation’s own measured pace once a panel has landed. An estimate, not a deadline — say “about”. Absent when nothing is left to render.
resolvedInputobjectoptional#661 — what the server actually resolved the request to, before any pixel was produced.
snapshotIdstringoptionalpsi_+ the first 32 hex of the fingerprint. Quote it in a bug report.fingerprintstringoptionalsha256 over every resolved fact. Equal fingerprints ⟹ equal production input.
sourcesarrayoptionalOne entry per source cell, resolved or not.
slotis the cell the bytes came FROM — a shot id and coordinate on a saved project, an ordered slot label on a direct render. Never a filename, a ref or a URL.originis absent exactly whenstatusis notresolved: there is nothing the cell came from.each item
object
slotstringrequiredstatusstringrequiredone of
resolvedmissingambiguousoriginstringoptionalone of
inline_base64stored_refremote_urllocal_pathproject_stored
defaultsAppliedarrayoptionalWhat the server chose because the caller said nothing.
each item
string
overridesarrayoptionalWhat the caller said that changed the outcome.
each item
string
readinessobjectoptional#665 — the readiness verdict and any waiver receipt.
statestringoptionalblocked⟹ nothing was delivered.ready_with_findings⟹ delivered, and here is what to know.one of
readyready_with_findingswaivedblockedpolicyVersionstringoptionalThe readiness policy this verdict was computed under. A waiver granted under another one is rejected.
findingsarrayoptionalEvery finding, with
code,waivableanddigest. A waiver names one id AND its digest; there is no wildcard and no code-level waiver.each item
object
idstringrequiredrf_+ 24 hex. Quote it in a waiver.severitystringrequiredone of
infowarnblockmessagestringrequiredtargetobjectrequiredThe exact shot, panel, locale, output or frames this finding is about.
shotIdstringoptionalpanelIndexnumberoptionallocalestringoptionaloutputstringoptionalcaptionLayernumberoptionalframesarrayoptionaleach item
number
evidenceobjectrequiredBounded redacted facts used to derive the finding and its digest.
waiversarrayoptionalThe waivers this call ACCEPTED. Pass these objects back verbatim to reuse them; a rebuilt one is rejected.
each item
object
findingIdstringrequiredfindingDigeststringrequiredpolicyVersionstringrequiredwaivedBystringrequiredwaivedAtstringrequiredcodestringrequiredone of
source_identity_ambiguousoutput_unsupportedoutput_omittedlook_exact_unavailableno_panelspanel_invalid_pngpanel_dimensions_invalidrenderer_failedinput_changed_during_runpanel_source_emptylocale_source_fallbackdevice_source_fallbackcaption_inheritedcaption_locale_fallbacklook_source_fallbackcaption_device_collisioncaption_caption_collisiondevice_device_collisioncaption_legibility_unresolvedscope_filteredlocale_not_live_on_applelook_hold_inactivecaption_text_emptyreasonstringoptional
artifactsarrayoptionaleach item
object
idstringrequiredThe opaque asset id from the custody registry (#663) — or, for
kind: "share", the link’s own token. Never a storage path, and never a signed URL.kindstringrequiredone of
panelbundlescreenshotshareprojectdeliveryobjectrequiredmodestringrequiredone of
inline_base64signed_urllocal_pathshare_linkurlstringoptionalTHE ONE FIELD a signed URL may appear in. Nothing else in this envelope carries one.
expiresAtstringoptionalISO 8601, when the grant above stops working.
bytesnumberoptionalretainedUntilstringoptionalISO 8601, when ShotOps stops keeping the artifact itself.
failureobjectoptionalPresent exactly when
statusisfailedorrefused.codestringrequiredThe closed failure code. Branch on this, never on the sentence.
one of
invalid_inputunsupported_inputproject_not_foundproject_ambiguousno_projectsversion_not_foundresolution_incompletereadiness_requiredreadiness_unmetasset_not_foundasset_in_useauthentication_requiredplan_requiredtrial_choice_requiredquota_exhaustedanonymous_limit_reachedcapability_deniedbilling_unavailablerender_failedstorage_failedpersistence_failedupstream_unavailablepayload_too_largerate_limitedoperation_not_foundidempotency_conflictoperation_unavailableoperation_not_readycontract_version_unsupportedinternal_errorphasestringrequiredHow far the call got.
inputandauthorizationguarantee nothing was rendered, stored, written or charged.one of
inputauthorizationresolutionreservationexecutiondeliverypersistenceretryablebooleanrequiredtrue ⟹ the identical call may succeed later with nothing changed.
nextActionstringrequiredThe one move that resolves this, machine-readable.
one of
nonefix_inputchoose_projectupload_assetsreduce_scopesign_inchoose_planupgrade_planretrywait_and_retrypoll_operationupgrade_clientcontact_supportdetailsobjectoptionalBounded, redacted, code-specific facts (the offending field name, the candidate project ids, the required plan). Never a credential, a signed URL or screenshot bytes.
panelsarrayoptionaleach item
one of 3 shapes
1
string2
refrefstringrequiredurlstringrequiredformat uri
3
positionpositionintegerrequiredmin 1contentIndexintegerrequiredmin 1mimeTypestringrequiredalways "image/png"deliverystringrequiredone of
imageresource_linkrefstringoptional
operationIdstringoptionalstatusstringoptionalone of
acceptedqueuedrunningcancel_requestedsucceededfailedcancelledprogressobjectoptionalcompletednumberrequiredtotalnumberrequiredpanelsCompletednumberoptionalPanels finished — the number to report to the user.
panelsTotalnumberoptionalPanels this operation will produce.
attemptnumberrequiredheartbeatAtstring or nulloptionalestimatedRemainingSecondsnumberoptionalAbout how many seconds of rendering are left. An estimate — say “about”.
failureanyoptional
Example
That request maps to this call:
{
"name": "render_strip",
"arguments": {
"screenshots": [
{
"ref": "uploads/you/tidebook/01-today.png"
},
{
"ref": "uploads/you/tidebook/02-forecast.png"
}
],
"style": {
"layout": "standard",
"captions": [
{
"text": "Know the tide before you go"
},
{
"text": "Seven days of swell"
}
]
},
"preview": true
}
}One base64 PNG per panel, the panel size it rendered at, and — when the composition needs attention — a layout block naming every caption sitting on a device and, where the repair is deterministic, a corrected look you can pass straight back.
Access and cost
- Preview renders are open to everyone, including a hosted caller with no account.
- A full-resolution render is production output: it needs an active Pro trial or Pro, and the server decides that before any pixel is rendered or any cloud credit is spent.
- On the hosted server a preview panel costs 1 cloud credit and a full-resolution panel 4. On the local stdio server rendering is free and spends nothing.
- Without an account the hosted server allows 3 preview attempts per network per 30 days, up to 5 panels each.
When it refuses
- The reply says production output is not available.
- That is an entitlement answer, not a render failure — nothing was rendered and nothing was charged. Call account_status, do the step it names, then retry. Previews keep working.
- A full-resolution screenshot is rejected as too large.
- Inline base64 is capped for small payloads. Import the file first (import_screenshot for a URL or a ChatGPT attachment, request_screenshot_upload for a file on the user’s disk) and pass the returned ref instead.
- The result carries a
layoutblock full of findings. - The composition measured badly — captions are landing on devices. Pass
layout.correctedLookback aslookand re-render, or pick a different layout template from the longest headline in the set. - A
pathentry is refused. - Reading a file off disk only works on the local stdio server. On hosted, use a URL or a signed upload.
Where this fits
What the agent is told
The title and description the server publishes on tools/list — this is the copy a model chooses between, reproduced verbatim.
Render App Store screenshots
Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "…" }] } — each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project's saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": … } entries — `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first — inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look — it returns the five palette presets, the three layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` FROM THE LONGEST HEADLINE IN THE SET, once, before anything else: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The text region is RESERVED, NOT FITTED — held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone — the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` — pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so — `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.