Call every new inbound contact within seconds of form submission, then automatically update the deal stage and book an appointment based on the call outcome.
Time to build: 30 minutes (requires HubSpot Operations Hub Professional for custom-coded workflow actions — or use the simpler webhook-based flow below if you're on Marketing Hub Starter).
Option 1: HubSpot Operations Hub Professional (recommended)
Step 1 — Add custom properties
In Settings → Data Management → Properties, add to the Contact object:
| Label | Internal name | Type | Purpose |
|---|---|---|---|
| Volume Reach Call ID | vr_call_id | Single-line text | Links the HubSpot record to the call. |
| Volume Reach Last Outcome | vr_last_outcome | Dropdown (INTERESTED, NOT_INTERESTED, CALLBACK, DNC, NO_ANSWER, BUSY, VOICEMAIL, WRONG_NUMBER, NOT_OWNER) | Last call result. |
| Volume Reach Last Summary | vr_last_summary | Multi-line text | One-sentence summary from extraction. |
| Volume Reach Last Transcript | vr_last_transcript | Multi-line text | Full transcript. |
| Appointment Scheduled | vr_appointment_iso | Date picker | Scheduled appointment ISO. |
| Budget Range | vr_budget_range | Dropdown (under_5k, 5k_to_15k, 15k_to_50k, 50k_plus, not_mentioned) | From extraction. |
Step 2 — Create a HubSpot workflow
Workflows → Create workflow (from scratch) → Contact-based.
- Enrollment trigger:
Form submissions > <your lead form>→ "has been filled out". Optionally addPhone number > is knownas a filter to skip leads without a phone.
Step 3 — Action: Custom-coded "Trigger Volume Reach call"
Add a Custom code action.
Secrets:
VR_API_KEY— your Volume Reach key (vr_live_...).VR_AGENT_ID— the agent's ID.
Inputs to the action:
phone→ Contact propertyphonefirstname→ Contact propertyfirstnamelastname→ Contact propertylastnameemail→ Contact propertyemailcontactId→ Contact propertyhs_object_idaddress→ Contact propertyaddress(if applicable)
Code (Node.js 18):
exports.main = async (event) => {
const apiKey = process.env.VR_API_KEY;
const agentId = process.env.VR_AGENT_ID;
const { phone, firstname, lastname, email, contactId, address } = event.inputFields;
if (!phone) {
return { outputFields: { vr_call_id: null, status: "skipped_no_phone" } };
}
const body = {
agentId,
phoneNumber: phone,
variables: {
leadName: [firstname, lastname].filter(Boolean).join(" ") || "there",
leadEmail: email || "",
propertyAddress: address || "",
},
metadata: {
hubspot_contact_id: contactId,
},
idempotencyKey: `hs-contact-${contactId}-v1`,
knownConsent: true,
consentRecordId: `hubspot-form-${contactId}`,
};
const response = await fetch("https://api.volumereach.com/api/v1/public/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
return {
outputFields: {
vr_call_id: null,
status: `failed: ${err?.error?.code || response.status}`,
},
};
}
const data = await response.json();
return {
outputFields: {
vr_call_id: data.call.id,
status: "queued",
},
};
};
Step 4 — Action: Copy vr_call_id to contact property
Add a Set property value action mapping vr_call_id from the previous step to the vr_call_id contact property. This gives HubSpot the link between the contact and the call, for later enrichment.
Step 5 — Review + turn on the workflow.
Every time a contact fills out the form, HubSpot will trigger a call within a few seconds.
Step 6 — Handle the webhook (separate endpoint)
HubSpot workflows handle the outbound trigger; you still need to handle the call.completed webhook to update the contact properties.
Register a webhook endpoint pointing at your backend (or a serverless function), subscribe to call.completed, and have the handler:
- Verify the
X-Webhook-Signature. - Look up the contact in HubSpot by
metadata.hubspot_contact_id. - Update properties using the HubSpot Contacts API:
// sample handler — simplified
async function handleCallCompleted(data) {
const hubspotId = data.metadata?.hubspot_contact_id;
if (!hubspotId) return; // not from a hubspot trigger
const properties = {
vr_last_outcome: data.outcome,
vr_last_summary: data.extraction?.summary || "",
vr_last_transcript: (data.transcript || "").slice(0, 60000),
vr_appointment_iso: data.extraction?.appointmentDateTimeISO || "",
vr_budget_range: data.extraction?.budgetRange || "not_mentioned",
};
await fetch(`https://api.hubapi.com/crm/v3/objects/contacts/${hubspotId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ properties }),
});
}
Step 7 — Move deal stages based on outcome
Once properties are updated, HubSpot's native workflows can take over — create a second workflow triggered on "Volume Reach Last Outcome changes to INTERESTED" that moves the associated deal to "Sales Qualified Lead" and assigns an owner.
Option 2: HubSpot Marketing Hub Starter (no custom code)
If you don't have Operations Hub Professional, you can't run custom code directly in a workflow. Fall back to:
-
Use a webhook action in the HubSpot workflow to POST to Volume Reach.
- This requires Marketing Hub Professional + Operations Hub Starter, OR Enterprise.
- On Starter, you can chain through Zapier — see zapier.md.
-
Use HubSpot's native "Webhooks" feature (Settings → Integrations → Private Apps → Webhooks) to POST to your own webhook receiver, which then calls Volume Reach.
Production checklist
- Phone-number formatting: if your HubSpot form doesn't validate to E.164, add a formatter (HubSpot Operations Hub → Format → Phone number) before the Volume Reach step. Volume Reach rejects non-E.164 numbers with
400 invalid_phone_number. - Dedup: always include
idempotencyKey: \hs-contact-$-v1`. If you edit a contact's phone and re-enroll, bump the suffix to-v2` so we dial the new number. - Quiet hours: HubSpot workflows have a Delay until action — use it to only fire calls between 9 AM–7 PM in the contact's timezone. Volume Reach does not enforce quiet hours.
- Concurrent-call limit: if you import a big list and re-enroll 500 contacts simultaneously, Volume Reach may throttle with
429 concurrent_limit_exceeded. Add a workflow delay to spread the load. - Error handling: if the Volume Reach API returns an error, write the error code to a contact property and route those contacts to a HubSpot task list for manual review.
Sample webhook payload reference
See webhooks.md for the complete call.completed payload shape. Key fields for HubSpot:
data.call_id→ store asvr_call_id.data.outcome→ map to your lifecycle stage logic.data.metadata.hubspot_contact_id→ look up the contact.data.extraction.*→ populate custom properties.data.transcript→ store up to 60 000 chars (HubSpot long-text limit).data.recording_url→ always null in the webhook. Fetch fresh viaGET /calls/:idwhen a user clicks "Listen to recording" in HubSpot.