Volume Reach/Docs

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-$-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_urlalways null in the webhook. Fetch fresh via GET /calls/:id when a user clicks "Listen to recording" in HubSpot.