HeyGen + Salesforce Integration Guide 2026
By Umar Asghar, AI Integration Engineer - July 13, 2026
TL;DR
When a Salesforce Opportunity moves to a target stage, fire a callout to your webhook receiver, render a personalized HeyGen video, then PATCH the video URL back to a custom field on the Opportunity. Two trigger approaches: Record-Triggered Flow with an HTTP Callout Action (no code required, works in Sandbox, ships in hours) or an Apex trigger with an @future(callout=true) method (more control, required for bulk-safe implementations). This guide covers both, plus governor limits, sandbox testing, and the write-back REST call.
Before You Pick an Approach
You need: a Salesforce org on Enterprise Edition or above, a HeyGen account with API access (paid plan), a webhook receiver you control (a small Node/Python server or a hosted function - covered in Step 3), and a custom field on the Opportunity object. Create that field now: type URL, API name Personalized_Video_URL__c, label “Personalized Video URL”. Everything else follows.
One constraint to plan around before writing a single line: Salesforce does not allow synchronous HTTP callouts from trigger context. Every callout must be async. This is not a workaround - it is the architecture.
Approach 1: Record-Triggered Flow with HTTP Callout Action
Use this path if you do not have a Salesforce developer, want to avoid deployments, or need to ship inside a week. Flow’s HTTP Callout Action (available Summer ’23 and later) lets you define an external service call in the Flow Builder UI and invoke it as a step - no Apex required.
Note: Process Builder is deprecated. Do not use it. Any existing Process Builder automation should be migrated to Flow before adding new logic.
- In Setup, open Flows and create a new Record-Triggered Flow on the Opportunity object.
- Trigger condition: Record is updated. Entry condition:
StageNameequals your target stage (e.g., “Proposal/Price Quote”) AND the prior value was different. This prevents re-fires on unrelated field updates. - Run the flow: After the record is saved, in a new system context. This automatically makes the callout async - you cannot call an HTTP action from a before-save flow.
- Add an Action element, type HTTP Callout. Define the external service (your webhook URL, POST method, JSON body). Map these fields from the triggering record into the payload:
{!$Record.Id},{!$Record.Name},{!$Record.Account.Name},{!$Record.StageName}, and the associated Contact’s first name via a Get Records element.
// Example payload your Flow sends to the webhook receiver
{
“opportunity_id”: “0065g00000AbCdEAAZ”,
“opportunity_name”: “Acme Corp - Enterprise Deal”,
“account_name”: “Acme Corp”,
“stage”: “Proposal/Price Quote”,
“contact_first_name”: “Sarah”,
“contact_email”: “sarah@acme.com”
}
Approach 2: Apex Trigger with @future Callout
Use this path if you need bulk-safe handling, want to conditionally fire based on field logic that Flow cannot express cleanly, or your org already has developer capacity. The pattern: a thin trigger hands off to a handler class, which queues an async method.
// OpportunityTrigger.trigger
trigger OpportunityTrigger on Opportunity (after update) {
OpportunityTriggerHandler.handleAfterUpdate(
Trigger.new, Trigger.oldMap
);
}
// OpportunityTriggerHandler.cls
public class OpportunityTriggerHandler {
public static void handleAfterUpdate(
List<Opportunity> newList,
Map<Id, Opportunity> oldMap
) {
List<Id> opportunityIds = new List<Id>();
for (Opportunity opp : newList) {
Opportunity old = oldMap.get(opp.Id);
if (opp.StageName == ‘Proposal/Price Quote’
&& old.StageName != opp.StageName) {
opportunityIds.add(opp.Id);
}
}
if (!opportunityIds.isEmpty()) {
HeyGenCalloutService.fireVideoRender(opportunityIds);
}
}
}
// HeyGenCalloutService.cls
public class HeyGenCalloutService {
@future(callout=true)
public static void fireVideoRender(List<Id> oppIds) {
List<Opportunity> opps = [
SELECT Id, Name, StageName,
Account.Name,
(SELECT FirstName, Email FROM Contacts LIMIT 1)
FROM Opportunity
WHERE Id IN :oppIds
];
for (Opportunity opp : opps) {
String contactName = opp.Contacts.isEmpty()
? ‘’ : opp.Contacts[0].FirstName;
String payload = JSON.serialize(new Map<String, Object>{
‘opportunity_id’ => opp.Id,
‘opportunity_name’ => opp.Name,
‘account_name’ => opp.Account.Name,
‘contact_first_name’ => contactName,
‘stage’ => opp.StageName
});
HttpRequest req = new HttpRequest();
req.setEndpoint(‘callout:HeyGen_Webhook’);
req.setMethod(‘POST’);
req.setHeader(‘Content-Type’, ‘application/json’);
req.setBody(payload);
new Http().send(req);
}
}
}
Store your webhook URL in Setup > Named Credentials as HeyGen_Webhook. This keeps the URL out of code and handles authentication headers cleanly. Never hardcode an endpoint URL in Apex.
Step 3: The Webhook Receiver
Your webhook receiver is a small HTTP server that accepts the Salesforce payload and drives the HeyGen render. Validate the incoming request with a shared secret header before processing anything - Salesforce callouts do not have a built-in signature scheme, so a secret token in a custom header is the standard pattern.
// Node.js receiver (Express)
app.post(‘/webhook/salesforce’, (req, res) => {
const secret = req.headers[‘x-sf-secret’];
if (secret !== process.env.SF_WEBHOOK_SECRET) {
return res.status(401).json({ error: ‘Unauthorized’ });
}
const { opportunity_id, account_name,
contact_first_name, opportunity_name } = req.body;
// kick off async render - respond 200 immediately
res.status(200).json({ received: true });
renderHeyGenVideo({ opportunity_id, account_name,
contact_first_name, opportunity_name });
});
Respond 200 immediately and run the render async. Salesforce callouts have a 120-second timeout. A HeyGen render takes 1 to 5 minutes, so if you await the render inside the request handler, every callout will time out and log an error in Salesforce.
Step 4: Trigger the HeyGen Render and Poll for Completion
From your webhook receiver, POST to HeyGen’s generate endpoint with the Salesforce field values mapped to your template variables. Include test: true in every payload until you are ready to go live - test renders are watermarked but cost zero credits.
POST https://api.heygen.com/v2/video/generate
X-Api-Key: <your_heygen_api_key>
Content-Type: application/json
{
“video_inputs”: [{
“character”: {
“type”: “avatar”,
“avatar_id”: “your_avatar_id”,
“avatar_style”: “normal”
},
“voice”: {
“type”: “text”,
“voice_id”: “your_voice_id”,
“input_text”: “Hi [contact_first_name], excited to share our proposal for [account_name].”
}
}],
“dimension”: { “width”: 1280, “height”: 720 },
“test”: true
}
The response returns a video_id inside data. Store it alongside the opportunity_id, then poll GET /v1/video_status.get?video_id={video_id} every 30 seconds until data.status is completed or failed. Cap retries at 15 (7.5 minutes). If status is still processing after the cap, log the opportunity_id and alert - do not retry blindly.
At higher volumes, register a HeyGen webhook instead of polling: POST /v1/webhook/endpoint.add with event types avatar_video.success and avatar_video.fail. HeyGen POSTs the completed video URL directly to your endpoint, eliminating polling loops entirely.
Step 5: Write the Video URL Back to Salesforce
Once the render completes, data.video_url has the hosted URL. Write it to the Opportunity using the Salesforce REST API. You will need a connected app with OAuth 2.0 credentials - or a named credential if you are calling back from a server you control.
PATCH https://<instance>.salesforce.com/services/data/v58.0/sobjects/Opportunity/<opportunity_id>
Authorization: Bearer <access_token>
Content-Type: application/json
{
“Personalized_Video_URL__c”: “https://res.cloudinary.com/heygen/video/…”
}
A successful PATCH returns HTTP 204 with no body. Log any non-204 response with the opportunity ID - a 404 means the record was deleted between trigger and write-back, a 401 means your access token expired. Use a refresh token flow or a long-lived JWT bearer grant to avoid token expiry issues in production.
One caveat on the video URL: HeyGen’s hosted URLs expire after 7 days. For same-day outreach the direct URL is fine. If your sales cycle is longer or you need the URL to remain valid in Salesforce indefinitely, download the video and re-host it to S3 or Cloudinary before writing the URL back.
Salesforce-Specific Error Handling
Three governor limit scenarios that differ from a standard webhook integration:
Bulk trigger firing
Data imports and batch jobs can update hundreds of Opportunities in one transaction. The @future method accepts a List<Id> deliberately - collect all qualifying IDs in the trigger, pass them to one @future call, and loop inside the async method. Never call @future per-record in a loop - that fires N async jobs and hits the 50 async calls per transaction limit.
Callout exception handling
Wrap every Http().send() call in a try-catch for System.CalloutException. Log failures to a custom object (e.g., HeyGen_Error_Log__c) so you have an audit trail without relying on Apex debug logs, which are ephemeral.
Chained async limitation
You cannot call another @future method from within a @future method. If you need to poll or chain logic (fire render, wait, write back from Apex), switch to Queueable Apex which supports chaining via System.enqueueJob().
Testing: Sandbox First
Never build this in production. The testing sequence:
- Deploy to a Full or Partial Copy Sandbox. Create a test Opportunity and manually move the stage. Confirm the trigger fires and the payload arrives at your receiver (check your server logs first, not Salesforce debug logs).
- With
test: truein the HeyGen payload, confirm a watermarked render completes and the video URL appears inPersonalized_Video_URL__con the Sandbox Opportunity. - Run a bulk test: import 10 Opportunities via Data Loader with the target stage. Confirm all 10 trigger without hitting governor limits. Check the async job queue in Setup > Apex Jobs.
- Remove
test: trueonly in production, after the Sandbox run passes.
For Apex unit tests: mock the HTTP callout with a class that implements HttpCalloutMock. Salesforce requires 75% code coverage to deploy, and live callouts are not permitted in test context - mocking is mandatory, not optional.
Already Have Salesforce? Skip the Build and Start Sending
If you have Salesforce in production and want this running without pulling in your dev team, the Pipeline Audit looks at your current org setup and outputs a blueprint specific to your edition, your stage names, and your existing automation. 30 minutes. No commitment.
Get your Pipeline Audit
FAQ
Should I use Flow or an Apex trigger?
Flow first. If your Salesforce org has a developer, Apex gives you finer control over governor limit budgeting and retry logic. But for most orgs the Record-Triggered Flow + HTTP Callout Action path ships faster and requires no deployments. The only time you must use Apex: you need synchronous field validation before the callout, or you are calling more than one external endpoint per trigger and need to sequence them.
What are the Salesforce governor limits I need to worry about?
Two main ones. Callouts per transaction: 100 max, so if your trigger fires on bulk updates (data imports, batch jobs) you will hit this fast - use @future or Queueable to push callouts async. @future methods per org per hour: 250,000 across all async jobs, which is generous but real under heavy load. Also: @future methods cannot chain, and you cannot call another @future from inside one. If you need chained async logic, use Queueable Apex instead.
How do I test this without burning HeyGen credits?
Two layers. First, always test in a Salesforce Sandbox, not production - Sandbox lets you fire triggers against test Opportunity records without touching real deals. Second, include test: true in every HeyGen API payload during development. Test renders produce a watermarked video but consume zero credits. Only remove test: true when you are validating the final production flow.
Which Salesforce edition supports HTTP callouts from Flow?
Enterprise Edition and above. Professional Edition does not support Flow HTTP Callout Actions or Apex development. If you are on Professional, you need a middleware layer - n8n or Make listening on a webhook, triggered by a HubSpot-style outbound message. Unlimited and Developer editions support everything covered in this guide.
ABOUT THE AUTHOR
Umar Asghar is the CTO of Kastiv. He builds production AI video and API integration pipelines.