Scope: inbound and outbound, with browser audio over WebRTC. Verified end to end against a live Vobiz account and a live Pipedrive sandbox — both directions connect with two-way audio and activities appear in the CRM.
What you get
How it works
The one thing to understand first: the browser is the A leg. The page sends the SIP INVITE itself, and the backend answers<Dial><Number> to reach the customer.
Requirements
Step 1: Prove the Vobiz account
Place a call with rtc-demo.vobiz.ai. If that fails, nothing here will work and you will debug the wrong layer for a day.Step 2: Create the SIP endpoint
Step 3: Run the backend
TUNNEL_URL, then check the answer URL is alive before touching any UI:
<Response> containing <Dial …><Number>. Anything else — an ngrok interstitial, a 404 page — and every call dies.
Step 4: Point Vobiz at the backend
Create an application whoseanswer_url is $TUNNEL_URL/answer, bind the SIP endpoint to it, and attach a DID for inbound:
Step 5: Connect Pipedrive over OAuth
In Developer Hub → Create public app, set the callback URL to$TUNNEL_URL/auth/pipedrive/callback — it must match PIPEDRIVE_REDIRECT_URI exactly, and only one callback URL is allowed per app.
Request these scopes and nothing more. Over-scoping is a documented rejection reason:
Put the Client ID and Secret in
.env, visit $TUNNEL_URL/auth/pipedrive and approve. GET /auth/pipedrive/status should then report "connected": true.
Step 6: Sign in and call
Open the panel. Before credentials are entered it shows NOT SIGNED IN.
The panel before sign-in.

Registered. The badge reads READY and the signed-in account is shown.
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.
Selecting the outbound caller ID.
Place the call
Type a number in E.164 format or use the built-in dialpad.
Entering the destination number on the dialpad.

An active call, with in-call controls and the matched CRM contact.
Inbound calls
A call to the attached DID raises an in-panel prompt. Enter accepts, Escape declines.
The inbound prompt, with keyboard shortcuts.
When the call ends
A summary confirms the duration and — the part that matters — that the call reached the CRM.
Call finished, with the CRM sync confirmed on the card itself.

Recordings, with inline players.
Backend routes
Testing
9 passed, 0 failed, 0 skipped.
Security
- Recording playback is HMAC-signed with a short expiry and carries no credentials. It previously took a
urlparameter 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. - The browser never holds the Vobiz Auth Token. It is POSTed once at sign-in and exchanged for an opaque session token.
- CORS is an allowlist, not
*. - OAuth tokens live in
backend/.pipedrive-tokens.json, mode0600, gitignored. That file holds a refresh token — a long-lived credential for the customer’s entire CRM.
Troubleshooting
The backend log is the honest account. The field that matters:DialBLegUUID is the single most useful field in this stack. Present means it connected; empty means no B leg was ever created, whatever the UI showed.
Three client settings in
agent-phone/vobiz-sip.js are not optional, and all three fail in ways that point nowhere near the cause:
Ignore the Endpoint API’s
sip_registered. It reads "false" even when registration genuinely succeeded, on every endpoint on the account. The panel gates on JsSIP’s registered event instead.Quick tunnels are the most common cause of “it stopped working.” The hostname changes on every restart, and TUNNEL_URL, the Vobiz application’s answer_url and the Pipedrive OAuth callback all go stale together.Next steps
- Clone the app: vobiz-ai/internal-pipedrive-crm
- The same softphone in other CRMs: Zendesk, HubSpot, Freshdesk
- Reference:
<Dial>,<Record>, SIP endpoints, CDRs