Home
Blog
Rendered, not recorded
Camera moves that make a website look like a product
Making bloom glow colors, not whites
Recording pages behind a login
Your product video as a build artifact
Sponsored
Aveiro
aveiro.app
TrademarkTrademarkTrademarkTrademark
Ctrl k
Search…
Sign up
Engineering

Your product video as a build artifact

Product videos go stale the moment you ship. Frametic's API exists so the video is regenerated by the deploy instead of remade by hand — one call to re-film, idempotent retries, webhooks you don't have to trust, and a price you can check before you spend it.
Updated 12d ago
Recording pages behind a login
Sponsored
Aveiro
aveiro.app
Sponsored
Aveiro
aveiro.app
Sponsored
Aveiro
aveiro.app
TrademarkTrademarkTrademarkTrademark
FrameticDoplerAveiro
© Dopler. All rights reserved.
Built with Aveiro
Every product video has the same defect: it is a photograph of a UI that has since moved on. You ship a redesign on Tuesday and the film on your landing page becomes a small lie by Wednesday. Fixing it means opening the tool, re-finding the framing, exporting, re-uploading — twenty minutes that nobody schedules, so nobody does it. The video ages in public until someone finally complains. The answer is not a faster editor. It is to stop treating the video as a document you maintain and start treating it as a build artifact: something regenerated from source, by a machine, on a trigger. That is what the Frametic API is for.

One call films the site again

A project holds the whole recipe — the URL, the camera, the look, the export shape — as one JSON document. Once it exists, re-filming it is a single POST: recapture: true is the part that makes this a deploy hook rather than a re-export. It re-runs the capture pass — a headless browser loads your site as it is now — and then renders the same scene over the new screenshot. Nothing about the framing has to be restated, because none of it changed. Only the page did. The call returns 202 as soon as the job is queued. It does not hold the connection open while a browser boots, and the render worker does the capture as the first step of the job it already claimed. A side effect worth knowing: a project can be rendered without ever having been filmed, because the variants a capture will write are known from the config before the capture that fills them exists.

Retries are supposed to be boring

CI retries. Deploy jobs get re-run because a flaky step failed three minutes after this one succeeded. If a retry meant a second render, integrating with a render API would mean writing a distributed lock before you wrote your first call. So jobs are idempotent per idempotencyKey. Post the same key twice and you get the same jobs back, not a duplicate and a second charge. Use the commit SHA, or the deploy id — something your pipeline already has and already treats as unique. Where a project captures a theme matrix, each variant gets its own idempotency scope, so a light/dark pair is two jobs that each retry independently rather than one job that can only be half-repeated.

The webhook is delivery, not truth

When a job finishes, webhookUrl receives a POST carrying x-frametic-event: render.finished, the job id and status, a playback URL, and a signed download URL. It is signed: x-frametic-signature is an HMAC over the exact payload bytes, using the webhook secret shown in Settings → API keys. Verify it before you trust it. The download URL is deliberately signed rather than key-authenticated. An integration ingesting the bytes — Aveiro copies finished videos onto its own CDN so a scheduled post never depends on us being up — hands the URL to a generic downloader with no headers to arrange, and that only works if the authority is in the URL itself.
Webhooks are a notification, not a source of truth
Deliveries are retried, but a misconfigured endpoint, a timeout or a deploy gap can still lose one. Confirm state with GET /api/v1/renders/:id, which carries the same signed download URL on purpose — so a lost webhook is reconciled by polling, not by stranding a paid render as forever-pending.
That symmetry is the whole design. The poll route is not a lesser fallback that returns less information; it returns exactly what the webhook would have, so the ingest path in your code can be one function called from either place.

Footage, or the cut

Two fields on the same endpoint answer two genuinely different questions. shots gives you footage: several framings of the same capture, one render each. A close-up of the header, a wide of the whole page, a push on the sidebar. Each shot's scene is merged over the project's, so a shot states only where the camera is and inherits everything about how it looks. That inheritance is the point — it is what makes the results cuttable together instead of a folder of unrelated exports. reel gives you the cut: those shots assembled into one video, across several projects. The difference between the two is not packaging. A reel's transitions overlap adjacent shots, so the edit only exists once every shot lives in the same composition — you cannot produce it by concatenating clips afterwards. Reach for shots when you want footage to edit, and reel when you want the trailer. The transitions themselves are chosen from what changed between neighbouring shots: a dissolve when the page changes, a slide when the camera crosses one, a hard cut when only the framing does — and never the same kind twice in a row. They also eat time rather than adding it. A one-second dissolve plays the tail of one shot and the head of the next simultaneously, so a cut reel is always shorter than the sum of its shots. The look and the export shape are reel-wide by design, and a shot cannot override them. A reel is one video; a per-shot width would have nowhere to go.

Ask for an outcome, not a resolution

Most calls do not want to compose a width, a height, an fps and a duration by hand. They want a thing: a social announcement, a launch hero, a light-and-dark pair for a site that ships a dark mode. So bundles are one-call recipes that expand into the several renders the job actually needs — social-announcement returns a vertical and a square cut of the same shot, theme-pair returns both themes at Full HD. Each variant is an ordinary render job underneath, so the output ceiling, the charge, idempotency and webhooks behave exactly as they do for a single render. GET /api/v1/renders/estimate lists what is available.

Know the price before you spend it

Anything that bills in its own currency needs to price a job before it charges for it, and any agent deciding whether to render needs to know if it can afford to. POST /api/v1/renders/estimate answers both without enqueuing anything, and returns your credit balance alongside the cost, so "can I afford this" is one call rather than two. The arithmetic is deliberately not documented as a formula for you to reimplement — the endpoint runs the same function the enqueue path bills with, which is the only way an integrator's number cannot drift from ours. It works out roughly like this:
Render
Credits
Full HD or smaller1
Up to ~QHD2
4K and above3
Longer than 10 secondsdoubles the above
A single frame (still)1
Stills are worth a mention of their own. Passing still: true renders one PNG of the same scene at roughly a hundred and fiftieth of the work — the cheap way to re-capture a project, check a framing, or generate an OG image from the same recipe that produces the video. The estimate is a snapshot and nothing more. The enqueue takes the credits, and between the two calls a concurrent render may have taken them first.

What this is actually for

Create a key under Settings → API keys and authenticate with Authorization: Bearer frametic_sk_…. API access unlocks with your first top-up. The thing worth building first is not the impressive one. It is the boring one: a step at the end of your deploy that posts a project id and a commit SHA, and a handler that swaps the video when the webhook lands. After that, the film on your landing page is as current as the site it is filming, and stays that way without anyone remembering to make it so. If you want to see the whole surface exercised at once, scripts/reel.mjs in the repository drives the entire workflow from a plan file — film each page, plan the rhythm, queue the reel — and runs entirely through the public API, so it obeys the same gates, quota and idempotency your own integration will.
curl -X POST https://frametic.app/api/v1/renders \
  -H "Authorization: Bearer $FRAMETIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "<id>",
    "recapture": true,
    "webhookUrl": "https://yourapp.com/hooks/frametic",
    "idempotencyKey": "deploy-8f21c3a"
  }'
{
  "projectId": "<id>",
  "reel": {
    "look": { "bloom": { "enabled": true } },
    "shots": [
      { "durationSec": 3,   "scene": { "cameraPreset": "orbit",    "framing": 1.15 } },
      { "durationSec": 1.2, "scene": { "cameraPreset": "dolly-in", "framing": 0.32 } },
      { "projectId": "<other-id>", "durationSec": 2.5, "scene": { "framing": 0.7 } }
    ]
  }
}