Skip to content

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

FieldTypeRequiredDescription
campaignstringYesWhich 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).
urlstringYesThe 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.
companystringNoCompany name, used as a headline fallback if first_name is missing, and available to the campaign's headline template as {company}.
first_namestringNoShown as an on-screen name caption, and available to the campaign's headline template as {first_name}.
emailstringNoThe 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.
titlestringNoHeadline 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 body
http
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
}
FieldDescription
email_htmlA 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_textThe plain-text equivalent, for the text part of a multipart email.
page_urlA 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_urlA 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_urlA still JPG of the same first moment as the GIF — used for link previews (og:image) in Slack, LinkedIn, etc.
video_urlThe raw MP4, byte-range enabled for seeking. Only page_url needs to be sent to a prospect; this is exposed for completeness.
durationThe video's length in seconds, measured at render time.
reusedtrue if this returned an existing render of the same site and campaign from the last 7 days, instead of rendering again.
processing_secondsHow 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).

errorMeaning
campaign_requiredThe campaign field was missing.
unknown_campaignNo campaign exists with that name. The response includes available, the list of active campaign names.
campaign_archivedThat campaign exists but has been archived.
clip_not_liveThe campaign's clip exists but hasn't been approved yet (Video → that campaign → Upload Clip).
url_requiredurl was missing.
invalid_urlurl couldn't be parsed as a URL at all.
site_unreachableThe site didn't load — DNS failure, timeout, a 500, or the URL pointed at a blocked internal address.
site_blockedThe site is behind a bot-check ("Checking your browser…") that a headless browser can't get past.
site_emptyThe page loaded but had little to no real content (a parked domain, for example).
scroll_blockedThe 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_failedSomething failed during recording or compositing.
video_not_configuredThe 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 error field (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_html carries 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.io as 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.

BluAuto