Appearance
Make a Personalised Video
Generate a personalised outreach video. This call is synchronous — it waits for the full render, which normally takes a few minutes, not milliseconds like Verify or Find.
There is a single endpoint. Which format it renders as — a face-cam circle over the prospect's site, or a full-screen demo with the site cut into a short intro — is decided by the campaign you send, not by a field in the request or by which endpoint you call.
Request
| Field | Type | Required | Description |
|---|---|---|---|
campaign | string | Yes | Which campaign to render against, for example plumbing. Forced lowercase and trimmed server-side, so MSP, msp and Msp all hit the same campaign. The campaign must exist and be active, and its clip must already be uploaded and approved in the app (Video → that campaign → Upload Clip). |
url | string | Yes | The prospect's website. https:// is assumed if missing. For a Walkthrough campaign, this is only ever shown for the few seconds of the intro window, not the whole video. |
company | string | No | Company name, used as a headline fallback if first_name is missing, and available to the campaign's headline template as {company}. |
first_name | string | No | Shown as an on-screen name caption, and available to the campaign's headline template as {first_name}. |
email | string | No | The prospect's email address. Stored alongside the render so you can see who each video went to in the app; it is not verified and nothing is sent to it from here. |
title | string | No | Headline shown above the GIF in email_html and on the video page. Omit it to fall back to the campaign's own headline template (Video → that campaign → Landing Page), if one is set. |
Every call also shows up as a lead against that campaign (Video → that campaign → Import Leads / Export Videos / Analytics) — there's no separate step to record it.
No field for the intro window's timing
For a Walkthrough campaign, exactly when the prospect's site appears and how long it stays is set once per clip, in the app — Video → that campaign → Upload Clip → scrub the preview → Set start / Set end → Save window. It's not part of the request. Every render for that campaign reads the saved timing fresh from the clip, so changing it on that page takes effect on the very next call, with nothing to change here.
Example
bash
curl https://app.bluauto.io/api/v1/video \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"campaign":"plumbing","url":"acmeplumbing.com","company":"Acme Plumbing","first_name":"John","email":"john@acmeplumbing.com","title":"Why Most Plumbers Lose Leads in 2026"}'python
import requests
resp = requests.post(
"https://app.bluauto.io/api/v1/video",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"campaign": "plumbing",
"url": "acmeplumbing.com",
"company": "Acme Plumbing",
"first_name": "John",
"email": "john@acmeplumbing.com",
"title": "Why Most Plumbers Lose Leads in 2026",
},
timeout=1200, # a render can take several minutes — set this generously
)
print(resp.json()["email_html"]) # ready-to-send email bodyhttp
POST /api/v1/video HTTP/1.1
Host: app.bluauto.io
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{"campaign":"plumbing","url":"acmeplumbing.com","company":"Acme Plumbing","first_name":"John","email":"john@acmeplumbing.com","title":"Why Most Plumbers Lose Leads in 2026"}n8n timeout
If you're calling this from an n8n HTTP Request node, raise its timeout to at least 1,200,000 ms (20 minutes) — the default is far too short for a queued render.
Response
json
{
"page_url": "https://video.bluauto.io/v/acme-plumbing-a7f3k2?l=8d1e2f6a-...",
"gif_url": "https://video.bluauto.io/f/acme-plumbing-a7f3k2/thumb.gif",
"poster_url": "https://video.bluauto.io/f/acme-plumbing-a7f3k2/poster.jpg",
"video_url": "https://video.bluauto.io/f/acme-plumbing-a7f3k2/video.mp4",
"email_html": "<div style=\"font-family:Arial…\">…</div>",
"email_text": "Why Plumbers Lose Leads\n\nWatch the video: https://video.bluauto.io/v/acme-plumbing-a7f3k2?l=8d1e2f6a-...",
"duration": 124,
"reused": false,
"processing_seconds": 187
}| Field | Description |
|---|---|
email_html | A ready-to-send email body — the title, the GIF as a big clickable thumbnail, and a plain-text fallback link, all inline-styled. Drop it straight into an email body; you never need to assemble the markup yourself. |
email_text | The plain-text equivalent, for the text part of a multipart email. |
page_url | A hosted, branded page with the video, subtitles, and a booking button — this is what email_html links to. Branding (colours, logo, button, custom HTML) follows whatever's set on the campaign's Landing Page step. Carries a ?l=<lead_id> so a play on this exact link is attributed back to this call's email/first_name/company — send it to this recipient only, since a forwarded link is indistinguishable from them opening it themselves. |
gif_url | A short animated GIF (under 1 MB) with the prospect's name on it. Already embedded in email_html; exposed separately if you want to build your own markup. |
poster_url | A still JPG of the same first moment as the GIF — used for link previews (og:image) in Slack, LinkedIn, etc. |
video_url | The raw MP4, byte-range enabled for seeking. Only page_url needs to be sent to a prospect; this is exposed for completeness. |
duration | The video's length in seconds, measured at render time. |
reused | true if this returned an existing render of the same site and campaign from the last 7 days, instead of rendering again. |
processing_seconds | How long this call took, in seconds, from request to response. Near-instant (a few seconds) when reused is true; a genuine render is normally a few minutes. |
Errors
Every failure returns a named error code so a workflow can branch (for example, to a text-only follow-up instead of giving up entirely).
error | Meaning |
|---|---|
campaign_required | The campaign field was missing. |
unknown_campaign | No campaign exists with that name. The response includes available, the list of active campaign names. |
campaign_archived | That campaign exists but has been archived. |
clip_not_live | The campaign's clip exists but hasn't been approved yet (Video → that campaign → Upload Clip). |
url_required | url was missing. |
invalid_url | url couldn't be parsed as a URL at all. |
site_unreachable | The site didn't load — DNS failure, timeout, a 500, or the URL pointed at a blocked internal address. |
site_blocked | The site is behind a bot-check ("Checking your browser…") that a headless browser can't get past. |
site_empty | The page loaded but had little to no real content (a parked domain, for example). |
scroll_blocked | The page loaded fine but doesn't respond to scrolling, usually a full-page slider or a custom scroll script. Failed deliberately rather than returning a video of a motionless page. |
render_failed | Something failed during recording or compositing. |
video_not_configured | The video pipeline isn't set up on this server yet. |
A 422 response with error: "site_blocked", "site_empty" or "scroll_blocked" may also carry "from_cache": true — meaning this exact domain already failed the same way recently, so it failed fast without attempting a fresh render. Those three are properties of the site itself, so retrying them gets the same answer.
POST /api/v1/video/demo has been removed
That was the old second endpoint for the full-screen format. There is now only one endpoint — the campaign you send decides the format. Calling /api/v1/video/demo returns 410 Gone with error: "endpoint_removed". Update any workflow still pointed at it to call /api/v1/video with a campaign field instead.
Using it in n8n
- Raise the HTTP Request node's timeout to at least 1,200,000 ms (20 minutes). A render is synchronous and can take several minutes; n8n's default timeout is far too short and will cut the request off before a response comes back.
- If n8n runs on the same server as the app, call it over the internal network (
http://bluauto:3000/api/v1/video) rather than going back out through the public domain — faster, and one less thing that can fail. - Branch on the
errorfield (see the table above) so a failed render still gets a text-only follow-up instead of the workflow just stopping.
Embedding the GIF in an email
Use email_html directly as the email body. It already contains the title, the GIF wrapped in a link to page_url, correct alt text, and a plain-text fallback link underneath, all with inline styles (Gmail strips <style> blocks from email bodies). There is nothing to assemble:
{{ $json.email_html }}Pass title in the request to set the headline above the thumbnail, or leave it out and let the campaign's own headline template (if set) fill it in per lead.
Replying through Instantly
To answer a prospect in the Instantly unibox, POST https://api.instantly.ai/api/v2/emails/reply and hand email_html / email_text straight to the body object:
json
{
"reply_to_uuid": "<the prospect's original email id>",
"eaccount": "you@yourdomain.com",
"subject": "Re: your site",
"body": {
"html": "{{ $json.email_html }}",
"text": "{{ $json.email_text }}"
}
}reply_to_uuid and eaccount come from the unibox event that triggered your workflow, not from this API. Instantly's reply endpoint accepts html and text and needs at least one of them.
Two things worth knowing
- Outlook and many Gmail setups block remote images by default. That's why
email_htmlcarries a text link under the image; without it a real share of prospects would see nothing clickable at all. Don't strip it out. - Keep
video.bluauto.ioas the only link in the email if you can. A brand-new domain showing up in a cold email carries a small spam-score cost; the fewer distinct domains, the better it lands.
What actually happens in each client: the video never plays inside the email — no email client on earth does that, which is exactly why a GIF links out to the real page instead. The GIF itself animates normally in Gmail, Apple Mail, and Yahoo. Outlook desktop only ever renders a GIF's first frame, which is why that frame already has a play button baked into it rather than fading in. Gmail fetches and caches the GIF through its own image proxy — reliable, but it means a cached copy won't refresh if the same URL is ever re-rendered with new footage.
For the shared response envelope (status codes, the API-key header) see Authentication.
