App media
Per-project brand assets — logo, app screenshots, demo videos, and end-card — the content pipeline uses so generated creative shows the customer's real product instead of hallucinated visuals.
The partner-facing app-media surface stores four asset slots per project:
logo— singleton; uploading a new one soft-deletes the previous.screenshot— append-many.demo-video— append-many. Required to unlock theugc-remixcontent format.end-card— singleton; uploading pins the partner image as the live end-card and Layers' auto-generated end-card is bypassed on new generations.
Uploads are URL-based: hand Layers a public https URL and we fetch it through the SSRF guard, validate the response's Content-Type + size, persist the bytes to Cloudflare R2, and return the canonical asset row. There is no presign / finalize dance at the partner edge.
Per-kind constraints
| Kind | DB role | Append vs. replace | Mime allowlist | Byte cap |
|---|---|---|---|---|
logo | logo | Replace — uploading another logo soft-deletes the previous one. | image/png, image/jpeg, image/webp, image/svg+xml | 5 MB |
screenshot | app-screenshot | Append. | image/png, image/jpeg, image/webp, image/heic | 10 MB |
demo-video | app-demo | Append. Unlocks the ugc-remix content format. | video/mp4, video/quicktime, video/webm | 200 MB |
end-card | end-card | Replace — uploading pins as the live end-card. Layers also auto-generates an end-card per project; the partner upload wins. | image/png, image/jpeg, image/webp | 5 MB |
media_role is a strict label — it does not float across uses. A demo-video will not be picked as a screenshot; a logo won't be substituted for a screenshot. Upload once per slot you need.
Source URL requirements
httpsonly.http://,file://,ftp://,gopher://,data:are rejected withVALIDATION.- Publicly fetchable from Layers' egress. The SSRF guard resolves the hostname and rejects anything that maps to a private / link-local / loopback / metadata range. Embedded credentials in the URL are rejected.
- Redirects up to 3 hops are followed; every hop is re-validated against the SSRF guard.
Content-Typeis authoritative. Layers does not sniff file magic — if your upstream returnsapplication/octet-streamfor an mp4, the request rejects withVALIDATION. Configure the host to return the correct mime.- Byte cap is per-kind (see table). Upstream
Content-Lengthheader is honored when present; the stream is hard-capped at the same number, so a misleading header doesn't help.
Endpoints
POST /v1/projects/:projectId/app-media— upload from a URL. Returns201with the canonical asset row.GET /v1/projects/:projectId/app-media— list everything on the project, grouped by kind:{ logo, screenshots[], demoVideos[], endCard }.DELETE /v1/projects/:projectId/app-media/:mediaId— soft-delete. The R2 object is reaped within 24 hours; downstream content that referenced the asset retains its already-rendered output.
Idempotency
POST /v1/projects/:projectId/app-media honors the Idempotency-Key header. Replays return the cached 201 response, so safe to retry on connection errors without creating duplicate media_library rows.
What partners do not get
- No presign + finalize flow. Layers fetches the bytes server-side from your URL. There is no client-direct R2 PUT.
- No per-asset metadata write. The partner only sets
kind+url. Width, height, codec, etc. are probed server-side at fetch time.
See also
- Upload app media
- List app media
- Delete app media
POST /content/ugc-remix— consumesmedia_role = 'app-demo'rows.