Video testimonials¶
A visitor records themselves in the browser — no app, no upload dialog, no file
to find. It works on a hosted collection page and inside a collect_feedback
widget on your own domain.
Requires a plan that includes video testimonials.
Switching it on¶
Video is off until you ask for it, in one of two places. Either is enough, and each is checked on every single upload:
| Where | Switch |
|---|---|
| Collection page | Collect video on the page's settings |
| Widget | Collect video on a feedback widget |
Why a switch at all
Video costs money — transcoding time and non-refundable minutes from your
plan. A public key alone therefore authorizes nothing here: without one of
these two flags the upload endpoint answers 404, so nobody can spend your
allowance through an embed you never enabled.
The recorder only appears when three things are true: you switched it on, your plan carries the feature, and storage is reachable. Offering a record button that can only fail after somebody has filmed themselves is worse than showing no button, so the server decides this, per request, and the widget obeys.
What the visitor sees¶
- Record a video → the browser asks for camera permission.
- Preview, then Start recording. A timer runs and stops itself at the limit.
- Use this video, or Record again as often as they like.
- The clip uploads with a progress bar while they type anything they want to add.
- They agree to the display permission and send. With a clip attached, that permission also covers automatic captioning — see below.
Text becomes optional once a clip is attached — the recording is the testimonial, and demanding a caption from somebody who just filmed themselves is a strange thing to ask.
Every failure leaves the typed path working. No camera, refused permission, an old browser, an upload that times out — each one says so plainly and the text box still works. Nothing about video is ever required.
Limits¶
| Length | 60 seconds |
| Size | 100 MB |
| Formats | MP4, WebM, QuickTime |
| Resolution | up to about 2.1 megapixels (1080p either way round) |
The browser stops at these numbers and the server enforces them again by probing the file — over range requests, before downloading it, so a rejection costs a megabyte or two instead of a hundred.
Output is 854×480 with a generated poster frame, served from the CDN.
How the upload works¶
The file never passes through our application server. A 100 MB body would tie up a worker for the whole transfer.
sequenceDiagram
participant B as Browser
participant A as withfeedback
participant S as Object storage
B->>A: POST /uploads/videos/ (reserve)
A-->>B: presigned POST + capability token
B->>S: POST the bytes directly
B->>A: POST /uploads/videos/{id}/complete/
A->>A: transcode, poster, publish on approval
The upload_token is the only authorization for everything after the
reservation. The ids identify; they do not authorize. Both calls are safe to
retry — completing twice returns the current state and does not start a second
transcode.
Send either page_slug or widget_id on the reserve call. One of them
is required; a body with neither is a 400 that says so.
Nothing is public until you approve it¶
A pending video is private. Moderators watch it through a short-lived signed URL; the public playback URL does not exist yet. Approval is what promotes the file to public storage.
Withdrawal reverses all of it: the item leaves every embed, the object is deleted, and the CDN edge is purged. If any of that fails, the leftover is recorded so a sweeper can finish the job rather than the file quietly surviving.
Captions¶
Every published video carries captions. They are on by default and there is no switch — a testimonial nobody can follow with the sound off is a testimonial half your visitors skip.
How they are made, in order:
- When the clip finishes transcoding, its audio track alone is sent to a speech-to-text service to produce a transcript. Only the audio is sent — no name, email or other detail is attached to it.
- Nothing is sent until the visitor has pressed send with the consent sentence ticked. A clip that was recorded and abandoned, or submitted with the box unticked, is never transcribed. This is why the consent wording changes when a video is attached: transcription is a processing step the plain display sentence never mentioned.
- The transcript lands in your inbox next to the video, editable. Speech recognition gets accents and noisy rooms wrong, and the words end up on your marketing page, so a person corrects them before anything is public.
Captions are private until a person has read them
Two things must both be true before a caption is shown to a visitor: the video is approved and published, and a moderator has reviewed the words. Approving from the submission page — where the transcript is on screen — does both at once. Approving from the inbox list, the API or an agent publishes the video but leaves the captions private until you press Publish captions. Leaked wrong text is worse than a missing caption, because search engines read it.
The captions travel inside the widget payload as timed cues, not as a separate subtitle file. If you render the payload yourself, see the public API; the script tag and the React SDK build the caption track for you.
What the transcription service is told
The transcript is produced through OpenRouter, the same provider as the optional AI features — but note that OpenRouter's zero-retention routing does not apply to speech-to-text, so the audio is handled by the speech provider it routes to under that provider's own terms. This is a disclosure your own privacy notice should carry. See Privacy. Transcription can be turned off for the whole site if that trade is not one you want to make.
The CAPTCHA asymmetry¶
Worth knowing if you are integrating rather than embedding.
On a hosted page, the video reservation is the one public endpoint that refuses a missing or bad CAPTCHA token. It spends money before any human looks, and a page we serve can always mint a token — so refusing is safe there.
Inside the widget it cannot work at all. A reCAPTCHA v3 site key is bound to its domains, and the widget runs on your domain, so no token can be minted at any score. The widget path therefore records the CAPTCHA outcome as a signal and never refuses.
That is a deliberate trade, and it is only reasonable because five other limits sit in front of the same endpoint — a monthly minute quota, per-project and per-IP byte budgets, a cap on uploads in flight, a daily transcode cap, and the decode-cost cap above. None of them can be influenced by whoever is calling.
When custom domains ship, you will be able to supply your own reCAPTCHA keys for your own domain, and the widget path becomes a hard gate too.