GET /api/v1/sites/{siteId}/media lists images. POST …/media/upload returns a signed URL for PUT uploads. GET …/media/screenshot-styles lists mockup looks; POST …/media/screenshot films a public page (async — poll GET …/screenshot/{renderId} and read warnings); POST …/media/screenshot-login returns a sign-in link for login-gated pages (free). Persist the canonical delivery path from upload (`publicUrl`) in MDX or metadata — not a signed URL. GET list responses also include optional `displayUrl` for immediate previews.
Why this exists
Pages should illustrate with real files, never invented image URLs (made-up links 404 and render broken). Agents check the library first, reuse what's there, and upload only when a new image is genuinely needed. Files land in the same site media library the dashboard's upload dialog uses, so everything stays visible in the editor.
List site media
GET /api/v1/sites/{siteId}/media?cursor=0&limit=24
Authorization: Bearer av_live_…
url is the canonical unsigned reference — persist this in page content. displayUrl is signed for the current token holder and changes over time; use it only to preview or download right now, not to embed in MDX.
Wrap images in the two-<Column> envelope above so the editor recognises them as image blocks — spacing (paddingTop / paddingBottom) on the outer column, width on the inner (replace, alt, aspect ratio). A bare <Media /> renders but cannot be edited in the visual editor. Aveiro signs the src path when a page is rendered — visitors on a live site get a standing grant; members previewing a draft get a short-lived grant only when they have site access. The delivery route re-checks site liveness on every read (not whether the storage key belongs to that site), which is what lets gallery templates keep working: cloning copies content and media references, not the underlying files. Unpublishing revokes visitor access. Legacy absolute Supabase URLs stored before this change are still recognised on read.
Read media (delivery route)
Uploaded objects are not world-readable. Browsers and platforms fetch them through:
GET /api/media/<storage-key>[?k=<delivery-grant>][?t=<share-token>]
No bearer token — authorization is carried in the URL or, as a last resort, the caller's session cookie.
Grant
Who can read
None (avatars/ and organization-logos/)
Anyone — identity assets by design
?k= visitor grant
Anyone while the site named in the grant stays published — including media paths that still point at a gallery template source. Unpublishing revokes access.
?k= member grant
Dashboard or API caller who already had access when the URL was minted
?t= share token
Any consumer with no session — social platforms at post publish, email recipients at send time
Session cookie (no grant)
Signed-in org members when client-composed HTML references bare /api/media paths without a grant — for example campaign preview iframes
Page MDX, site settings, and folder listings store the unsigned path. Signing happens at read (published pages, dashboard editor/settings reads, dashboard-home recent-site covers, marketing showcase gallery covers, logos, collection listings, campaign draft HTML, social asset thumbnails, product storefront images in the editor and on live product pages / Products / Product Showcase blocks) or at outbound send (email HTML). Persist publicUrl / url from upload or list responses — not displayUrl.
The response matches the signed-upload flow: canonical publicUrl for MDX, plus path and name. Fetches are SSRF-guarded (HTTPS only, public IPs, redirect cap, size cap while streaming). Failed fetch attempts count toward a per-site daily attempt limit separate from successful uploads.
HTTP
Code
When
400
`VALIDATION_ERROR`
Missing or invalid URL
400
`BLOCKED_HOST`
Private or disallowed target
429
`RATE_LIMITED`
Daily image or fetch-attempt limit
504
`TIMEOUT`
Remote host too slow
MCP equivalent: aveiro_import_media_from_url.
List screenshot styles
Before ordering a screenshot, list the camera and backdrop presets Frametic exposes. Pass a style's id back as styleId on the screenshot POST; without one, the render uses the project's default look.
GET /api/v1/sites/{siteId}/media/screenshot-styles?featured=1&limit=24
Authorization: Bearer av_live_…
Required scope:content:write
200 OK
featured=1 narrows to the curated gallery (recommended for pickers). The response lists metadata only — the server resolves styleId when you queue the render. An unknown id returns 404 NOT_FOUND rather than silently falling back to the default look.
Capture a product screenshot
When a page should show the actual product — a docs page illustrating the screen it documents, or a blog post with a real pricing page — film a publicly reachable URL in a 3D device mockup and render one frame into the site's media library. This is asynchronous: a browser loads the page first, so the POST returns a pending render you poll until an image exists.
Use upload when you already have a file. Use screenshot when the illustration should be a live page. Use AI image generation when the illustration is a concept, not a screen.
Pass estimateOnly: true to price the render without starting or charging anything. A still costs 10 AI credits (flat — cheaper than a mockup video). Credits are debited when the render is queued and refunded automatically if it fails.
200 OK (estimate)
Start the render by omitting estimateOnly or setting it to false.
Look options (all optional — compose over each other):
styleId — preset id from GET …/screenshot-styles
theme — which light/dark capture to frame: "light" or "dark". Naming either also schedules the project to film both variants, so later shots can pick either without a refilm. Omit theme to frame the project's first captured variant (one still, one charge).
framing — camera distance (0.4–3; below 1 is tighter)
chrome — "none", "light", or "dark" browser chrome around the page
Capture options (how the page is filmed — persist on the project for later shots):
GET /api/v1/sites/{siteId}/media/screenshot/{renderId}
Authorization: Bearer av_live_…
Required scope:content:write
200 OK while rendering
200 OK when done
Read warnings before embedding. A page that bounced to a login form renders perfectly — the image is real, but it is a picture of a login screen. warnings is separate from error: the render succeeded.
Embed imageUrl in page MDX or metadata:
<Media src="https://…/site-media/SITE_ID/1754900000000-ab12cd.png" alt="Pricing page on desktop" />
Constraints
The URL must be publicly reachable — login-gated pages need a one-time sign-in via POST …/screenshot-login first
Pass recapture: true to re-film a page that changed since the last shot (costs an extra browser capture)
List styles with GET …/screenshot-styles when the shot should match a specific mockup look
theme selects among variants the project has filmed; naming "light" or "dark" expands the project to capture both
Omit theme unless you need a specific variant — otherwise the first captured variant is used (avoids double renders on multi-theme projects)
Typical turnaround is about a minute; keep polling GET …/screenshot/{renderId} rather than inventing URLs
Always check warnings on the poll response before embedding
When warnings mentions a login redirect, call POST …/screenshot-login, have the owner sign in, then retry with recapture: true
Sign in for login-gated pages
When a page redirects to a sign-in form, the screenshot still renders — but warnings tells you the image is a login screen. Automation cannot type passwords; instead, mint a sign-in link the page owner opens once. Frametic stores an encrypted session against the site, so every later screenshot of that site inherits the sign-in.
Give loginUrl to a person who can sign in. The link expires in about an hour. When interactive is false, the hosted browser is unavailable on this deployment and the page may ask for credentials by hand instead.
No credits charged — nothing is rendered.
After they confirm sign-in, retry POST …/media/screenshot with recapture: true on the same URL.
Limits
Image types: JPEG, PNG, WebP, GIF (no SVG — signed uploads bypass server-side sanitization)
Max size 10MB; the signed URL is valid for 2 hours
Uploads are capped per rolling day per site, and count against the organization's storage quota
Screenshot stills cost 10 AI credits each; debited when queued, refunded on failure
Errors
HTTP
Code
When
400
VALIDATION_ERROR
Unsupported type or size over the cap
403
FORBIDDEN
Missing scope or storage quota exceeded
429
RATE_LIMITED
Daily upload limit reached
402
QUOTA_EXCEEDED
Organization AI credits exhausted (screenshot queue)
501
NOT_CONFIGURED
Frametic screenshots not configured on this server
502
FRAMETIC_ERROR
Screenshot render failed upstream
404
NOT_FOUND
No such screenshot render for this site, or unknown styleId