Skip to main content
Agents call from a HubSpot contact record, talk in the browser, and HubSpot writes the call onto that contact by itself. Source code: vobiz-ai/internal-hubspot-crm — the widget, the Node backend, and the scripts that register the calling extension with HubSpot.
Scope: inbound and outbound, with browser audio over WebRTC. HubSpot creates the call engagement itself from the widget’s SDK messages, using the agent’s own session — so the app needs no CRM scopes and the backend never writes to HubSpot.

What you get

How it works

The one thing to understand first: the browser is the A leg. The widget sends the SIP INVITE itself, and the backend answers <Dial><Number> to reach the customer.
The intuitive design does not work. Originating to the customer over the REST API and then bridging the agent in with <Dial><User> is blocked platform-side: Vobiz builds a gateway URI it cannot itself parse and drops its own INVITE (tr_eval_uri(): invalid uri, blocking gw). The customer answers, hears ringback, then “the agent could not be reached”.Inbound is the exception — <Dial><User> is the only way to reach a registered endpoint, and it works.

Requirements

Step 1: Prove the Vobiz account

Place a call with rtc-demo.vobiz.ai before anything else. If that fails, nothing here will work and you will spend a day debugging the wrong layer.

Step 2: Create the SIP endpoint

Vobiz rewrites the username you submit. Send hsagent and the stored username comes back as something like hsagent1187694299145202883643. Read the stored username out of the response — that is what registers, and what VOBIZ_SIP_USER must contain. Registering the name you chose fails in a way that looks like bad credentials.
Use a separate endpoint per integration. An endpoint binds to exactly one Vobiz application at a time, so sharing one between HubSpot and another CRM silently breaks whichever was configured first.

Step 3: Run the backend

Put the tunnel URL in PUBLIC_BASE and restart. Then check the answer URL before touching any UI:
It must return <Response> containing <Dial …><Number>. A dead answer URL produces the exact symptom people blame on registration: the customer answers, hears ringback, then “the agent could not be reached”.

Step 4: Point Vobiz at the backend

With the widget signed in, one call does all three steps — creates the application, binds the endpoint, attaches the DID:
Two undocumented platform behaviours it works around:
  • The endpoint-binding field is app_id, not the documented application, which is silently ignored and still returns 202 "changed".
  • DID attachment is POST /numbers/%2B<e164>/application with {"application_id": …}. The + must be percent-encoded, and /Number/ returns a bare 401 Unauthorised that reads exactly like a credentials problem and is not one.

Step 5: Sign in and call

Open the widget in HubSpot. It opens on a CONNECTING badge while the SIP stack registers.
Vobiz Calling for HubSpot widget showing an orange CONNECTING badge, a Connecting banner, and step 1 Sign in to Vobiz with Vobiz account and SIP direct tabs plus Auth ID and Auth Token fields

The widget on load — CONNECTING, with the sign-in form beneath it.

Sign in with the Auth ID and Auth Token from the console under API credentials — the Auth Token is not your login password. The badge turns green and the widget reports the registered agent.
Widget showing a green READY badge, Ready registered as test-agent, the signed-in account SA_LAUUF1D9 marked ACTIVE with its caller ID, and the Choose a caller ID and Place a call steps beneath

Signed in. The badge reads READY and the endpoint shows as ACTIVE.

The SIP direct tab signs in with SIP credentials instead, for deployments that do not hand the widget a Vobiz account login.

Choose a caller ID

Calling from lists the numbers on the account. Carriers require a real number here to bridge a call to a mobile or landline.
The Calling from field expanded into a dropdown listing six Vobiz numbers available as outbound caller IDs

Every provisioned number on the account is selectable as the outbound caller ID.

Place the call

Type a number in E.164 format, or click any phone number in HubSpot and the panel dials it.
The Number to call field showing a suggestion list of phone numbers drawn from HubSpot records, in both E.164 and local formats

Numbers found in HubSpot are offered as suggestions in the dial field.

Once connected the widget swaps Call for Hang up and runs a timer.
Widget during a live call showing the Call and Hang up buttons side by side and a status line reading On call with an elapsed timer of eight seconds

An active outbound call, with the elapsed timer.

Inbound calls

A call to the attached DID raises an overlay over HubSpot. Enter accepts, Escape declines.
Full-screen incoming Vobiz call overlay showing the calling number with green Answer and red Decline buttons and a hint that Enter accepts and Escape declines

The inbound overlay, with keyboard shortcuts.

Recordings

Completed calls appear under Call recordings with their duration and timestamp. Play opens an inline audio player.
Call recordings list showing several one-minute entries with timestamps and Play buttons, and an audio player docked at the bottom

Recordings list with the inline player open.

Playback links are HMAC-signed with a short expiry and carry no credentials. An earlier build took a caller-supplied url and fetched it with the account’s Vobiz credentials attached — a credential-exfiltration primitive any web page could drive. Never reintroduce a caller-supplied URL here.

Provisioning a dedicated app and number

Rather than wiring the application by hand, the widget can create a dedicated Vobiz application and SIP endpoint for HubSpot and link a number to it, with the answer and hangup URLs already set.
The Provision Dedicated App and Number button above a Number to link to HubSpot field, with the call recordings list and an audio player below

Provisioning a dedicated application and number from the widget.

Paste the public tunnel URL into Public webhook / tunnel URL (HTTPS), put the DID in Number to link to HubSpot, and provision.

How a call reaches the CRM

HubSpot creates the engagement itself, from the SDK messages the widget sends — cti.outgoingCall, cti.incomingCall and cti.callCompleted — using the agent’s own session.
This is why the app needs no CRM scopes and why hubspotFetch in backend/hubspot-oauth.js has no callers. If server-side CRM writes are ever added, both the scopes and the install permissions change.

Backend routes

The answer XML

Troubleshooting

The widget showing a red Call failed Busy banner directly beneath the Call button, with the dedicated app and routing section below

A failed call reports its cause inline, under the dial field.

DialBLegUUID is the single most useful field in this stack. Present means the call connected; empty means no B leg was ever created, whatever the UI showed. It is logged on every /dial-status line.Ignore the Endpoint API’s sip_registered — it reads "false" even when registration genuinely succeeded, on every endpoint on the account. The widget gates on JsSIP’s registered event instead.

Known limits

  • The backend is single-account. It is bound to one Vobiz account through .env, and every agent who signs in shares one SIP endpoint. Two agents on one install register as the same endpoint and race for calls. agentId is self-asserted — it selects a label, not an identity.
  • Marketplace distribution is not set up. distribution is private; switching to marketplace needs listing assets that do not exist yet.
  • agents.example.json is vestigial. SIP credentials come from .env; nothing reads that file for routing.

Next steps