HeyGen + HubSpot Integration: Complete Setup Guide 2026
By Umar Asghar, CTO at Kastiv
TL;DR
- -HubSpot workflow fires a webhook when a deal stage changes. Your receiver extracts contact data and calls POST /v2/video/generate.
- -HeyGen renders asynchronously. Poll /v1/video_status.get or register a webhook via /v1/webhook/endpoint.add to catch the result.
- -Once the video URL is ready, write it back to a HubSpot custom contact property using PATCH /crm/v3/objects/contacts/{id}.
- -Past 50 videos a day, you need a proper queue with retry logic. Fire-and-forget from a webhook receiver will silently drop jobs.
Most HeyGen + HubSpot tutorials route through Zapier. Zapier works until it does not - typically around the 50 videos/day mark when task limits, webhook timeouts, and lack of retry logic compound into silent failures. This guide builds the direct path: HubSpot workflow to webhook receiver to HeyGen API to HubSpot contact record. No intermediary. Full control.
What you need before you start
Four things must be in place before writing a line of code.
HubSpot Professional or Enterprise. Starter does not include workflow webhook actions. You need Pro minimum for the automation you are building here.
HeyGen Creator plan or above. The API is available from Creator tier. Get your API key from the HeyGen dashboard under Settings > API. Confirm your plan’s concurrent video limit - this becomes relevant when you configure queue concurrency.
A defined trigger event. The most common choice is a deal stage change (“Proposal Sent” or “Demo Scheduled”). Know your exact trigger before building the workflow, because changing it later means re-mapping the webhook payload.
A HeyGen template with variables set up. In the HeyGen dashboard, create a template with a text variable for the contact’s first name (e.g., {{first_name}}). Note the template ID - you will need it in the API payload.
Step 1: Set up the HubSpot workflow webhook
In HubSpot, go to Automation > Workflows > Create workflow. Choose “Contact-based” and set the enrollment trigger to the deal stage change or contact property you decided on.
Add an action: “Send a webhook.” Set Method to POST and paste your receiver URL. Under “Request body,” switch to “Include all contact properties” or use custom properties. At minimum, include these tokens in a custom JSON body:
// HubSpot webhook custom body tokens
{
"contact_id": "{{ contact.hs_object_id }}",
"first_name": "{{ contact.firstname }}",
"last_name": "{{ contact.lastname }}",
"company": "{{ contact.company }}",
"deal_stage": "{{ deal.dealstage }}",
"deal_name": "{{ deal.dealname }}"
}Enable “Include secret” in HubSpot and add a shared secret header (e.g., X-HubSpot-Signature). Verify this header in your receiver to reject spoofed requests.
Step 2: Build the webhook receiver
The receiver does one job: accept the HubSpot payload, validate it, sanitize the contact data, and enqueue a video generation job. It must return 200 in under 3 seconds or HubSpot marks the webhook as failed.
// Node.js / Express receiver (simplified)
import express from 'express';
import { videoQueue } from './queue.js';
const app = express();
app.use(express.json());
app.post('/webhook/hubspot', async (req, res) => {
// 1. Validate the HubSpot signature
const sig = req.headers['x-hubspot-signature'];
if (!isValidSignature(sig, req.body, process.env.HS_SECRET)) {
return res.status(401).send('Unauthorized');
}
// 2. Extract and sanitize contact data
const { contact_id, first_name, company, deal_name } = req.body;
const sanitized = {
contact_id,
first_name: toTitleCase(first_name?.normalize('NFC').trim()),
company: company?.normalize('NFC').trim() || 'your company',
deal_name,
};
// 3. Enqueue - do NOT await the video generation here
await videoQueue.add('generate-video', sanitized);
// 4. Ack immediately
res.status(200).send('Queued');
});Run this on a persistent host - a serverless function with a cold start over 3 seconds will cause HubSpot to log failures even when the job eventually processes. A small DigitalOcean droplet or a Railway service works well.
Step 3: Call HeyGen POST /v2/video/generate
This is the core API call. Your queue worker picks up the sanitized contact data and submits the video generation request. The test: true flag generates a watermarked preview without consuming credits - use it during development.
// POST https://api.heygen.com/v2/video/generate
{
"video_inputs": [
{
"character": {
"type": "avatar",
"avatar_id": "your_avatar_id_here",
"avatar_style": "normal"
},
"voice": {
"type": "audio",
"voice_id": "your_voice_id_here"
},
"background": {
"type": "color",
"value": "#FFFFFF"
}
}
],
"variables": {
"first_name": {
"name": "first_name",
"type": "text",
"properties": {
"content": "{{first_name}}"
}
}
},
"template_id": "your_template_id_here",
"dimension": {
"width": 1280,
"height": 720
},
"test": true
}A successful submission returns HTTP 200 with a video_id in the response body. Store this ID against the contact_id in your database. You need both to complete the loop.
// Successful submission response
{
"code": 100,
"data": {
"video_id": "abc123xyz..."
},
"message": "Success"
}Step 4: Handle the async render
HeyGen does not render synchronously. You have two options to know when the video is ready.
Option A - Register a HeyGen webhook. Call POST /v1/webhook/endpoint.add with your callback URL. HeyGen fires a avatar_video.success or avatar_video.fail event when render completes. This is the preferred approach for production.
// POST https://api.heygen.com/v1/webhook/endpoint.add
{
"url": "https://yourdomain.com/webhook/heygen",
"events": ["avatar_video.success", "avatar_video.fail"]
}// Incoming webhook payload on success
{
"event_type": "avatar_video.success",
"event_data": {
"video_id": "abc123xyz...",
"url": "https://video.heygen.com/...",
"gif_url": "https://video.heygen.com/....gif",
"duration": 47.2
}
}Option B - Poll /v1/video_status.get. If you cannot expose a public webhook endpoint (internal tools, VPN-only environments), poll every 30 seconds with GET /v1/video_status.get?video_id=abc123xyz. Stop polling when status is completed or failed. Cap polling at 25 minutes and dead-letter anything that has not resolved by then.
Do both in production. Register the webhook for instant delivery. Run a reconciliation job every 30 minutes to poll any video_id that has been pending for over 10 minutes with no webhook event. Webhooks are notifications, not guarantees.
Step 5: Write the video link back to HubSpot
First, create a custom contact property in HubSpot. Go to Settings > Properties > Contact Properties > Create property. Name it something like “Personalized Video URL” with field type “Single-line text.” Note the internal property name (e.g., personalized_video_url).
When your HeyGen webhook fires or your poll finds a completed video, use the stored contact_id to update the HubSpot contact via the CRM API:
// PATCH https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}
// Headers:
// Authorization: Bearer YOUR_HUBSPOT_PRIVATE_APP_TOKEN
// Content-Type: application/json
{
"properties": {
"personalized_video_url": "https://video.heygen.com/share/abc123xyz",
"personalized_video_sent_date": "2026-07-13T14:30:00Z"
}
}Use a HubSpot Private App token, not an OAuth flow, for server-to-server writes. A successful PATCH returns 200 with the updated contact object. Once this property has a value, you can trigger a second HubSpot workflow to notify the sales rep via task or email.
Step 6: Error handling that actually covers the failure modes
Three failure types need distinct handling. Mixing them leads to burned credits, duplicate videos, or silently lost jobs.
429 Rate limit. HeyGen returns 429 when you exceed your plan’s concurrent video limit. Retry with exponential backoff: wait 2s, then 4s, then 8s, then 16s, then 32s. After 5 attempts, dead-letter the job and alert. Do not drop 429s silently - this is the most common cause of “missing” videos.
Avatar not found (400 / error code 40011). Your avatar_id is wrong or stale. Do not retry - the same request will fail again. Validate avatar IDs against GET /v2/avatars on startup and every 6 hours. On this error, alert immediately and write video_generation_failed to the HubSpot contact record so the rep is not waiting on a video that will never arrive.
Render failure (avatar_video.fail event). Retry once with the same payload. If the second attempt also fails, dead-letter. Render failures are usually transient infrastructure issues on HeyGen’s side, but repeated failures on the same payload indicate a data problem (invalid template variable, empty text field) that retries will not fix.
// Retry decision tree (pseudocode)
switch (error.type) {
case '429':
// Retry with backoff - transient capacity issue
await retryWithBackoff(job, { maxAttempts: 5, baseDelayMs: 2000 });
break;
case '400_AVATAR_NOT_FOUND':
// Do NOT retry - data error, alert and fail fast
await invalidateAvatarCache();
await alertOps('Avatar ID invalid', job);
await writeHubSpotFailure(job.contact_id, 'avatar_not_found');
break;
case 'RENDER_FAIL':
if (job.attempts < 2) {
await retryOnce(job);
} else {
await deadLetter(job);
await writeHubSpotFailure(job.contact_id, 'render_failed');
}
break;
}Where this pattern breaks: the 50 videos/day threshold
Everything above works cleanly at low volume. Past roughly 50 videos per day, you hit two structural problems that the code above alone does not solve.
HeyGen concurrent render limits. Your plan has a cap on how many videos can render simultaneously. At high volume, every submission beyond that cap gets a 429. If your queue workers all submit at full speed without a concurrency cap matching your plan tier, you will retry-storm yourself and exhaust your daily task budget doing so. Cap your worker concurrency to match your HeyGen plan limit.
HubSpot API rate limits. The HubSpot CRM API allows 100 requests per 10 seconds on Professional. Writing back 50 video URLs in a burst after a mass trigger event (e.g., a batch deal stage change) can hit this ceiling. Throttle your write-back calls or use the HubSpot Batch Contacts update endpoint (POST /crm/v3/objects/contacts/batch/update) to write up to 100 records per request.
Past 50 videos a day, you need proper queue infrastructure: BullMQ on Redis, Temporal, or a managed queue service. You also need a monitoring dashboard - a dead-letter queue that nobody watches is not error handling, it is deferring the problem.
Built something similar and it is breaking at volume?
The Pipeline Audit diagnoses it in 30 minutes. We map your current integration, identify exactly where jobs are dropping, and hand you a written fix plan - whether or not you hire us to build it.
Get your Pipeline Audit
Every build ships with the Working Pipeline Guarantee. Scoped, priced, and delivered fixed-price.
FAQ
Do I need a HeyGen Enterprise plan to use the API with HubSpot?
No. The Creator plan includes API access. You need at least HubSpot Professional for workflow automation (Starter does not include webhooks). Check your HeyGen plan's monthly video credit cap before you go to production - the API shares the same credit pool as the dashboard.
How long does a HeyGen video take to render before I can write the link to HubSpot?
Short videos (under 60 seconds) typically complete in 2-5 minutes. Longer videos or high-queue periods can take 10-20 minutes. Never block your webhook receiver waiting on the render. Submit the job, store the video_id, return 200 immediately, then handle the render-complete event asynchronously.
Can I trigger HeyGen from a HubSpot deal stage change instead of a contact update?
Yes. Set your HubSpot workflow enrollment trigger to 'Deal stage is changed to [stage name]'. Include the associated contact ID in the webhook payload using HubSpot's token syntax: {{ contact.hs_object_id }}. Your receiver then fetches the contact properties before calling HeyGen.
What happens to my HubSpot workflow if the HeyGen API is down or returns a 500?
HubSpot fires the webhook once. It does not retry on your API's behalf. Your receiver must handle this. On a 5xx from HeyGen, push the job to a retry queue with exponential backoff. If all retries exhaust, write a failed status to a HubSpot custom property so the sales rep knows the video was not sent.
ABOUT THE AUTHOR
Umar Asghar is the CTO of Kastiv. He builds production AI video and API integration pipelines.