Volume Reach

HubSpot Integration

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).


Step 1 — Add custom properties

In Settings → Data Management → Properties, add to the Contact object:

LabelInternal nameTypePurpose
Volume Reach Call IDvr_call_idSingle-line textLinks the HubSpot record to the call.
Volume Reach Last Outcomevr_last_outcomeDropdown (INTERESTED, NOT_INTERESTED, CALLBACK, DNC, NO_ANSWER, BUSY, VOICEMAIL, WRONG_NUMBER, NOT_OWNER)Last call result.
Volume Reach Last Summaryvr_last_summaryMulti-line textOne-sentence summary from extraction.
Volume Reach Last Transcriptvr_last_transcriptMulti-line textFull transcript.
Appointment Scheduledvr_appointment_isoDate pickerScheduled appointment ISO.
Budget Rangevr_budget_rangeDropdown (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 add Phone number > is known as 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 property phone
  • firstname → Contact property firstname
  • lastname → Contact property lastname
  • email → Contact property email
  • contactId → Contact property hs_object_id
  • address → Contact property address (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:

  1. Verify the X-Webhook-Signature.
  2. Look up the contact in HubSpot by metadata.hubspot_contact_id.
  3. 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:

  1. 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.
  2. 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-${contactId}-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 as vr_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 via GET /calls/:id when a user clicks "Listen to recording" in HubSpot.