Calls API — how it’s used
This page walks through how a CRM actually uses the Calls API day to day — placing a call, reacting to what happens next, and controlling it while it’s live. For the full method-by-method reference, see the Calls API reference page.
agentUuid. See the reference page’s setup steps if you haven’t done this part yet. What you can do with it
Connecting before you can call anything
A call can only be placed once the client has connected and registered the agent’s line:
const client = new BelsmartCrmClient({ apiBaseUrl: "https://pbx.example.com", apiKey: "pk_...", apiSecret: "...", agentUuid: "<mapped agent uuid>", }); await client.init();
client.call() won’t attempt to dial if the client isn’t connected — it rejects immediately instead. In practice, your CRM’s call button should stay disabled until the client reports it’s ready:
client.on("connection_status", ({ status }) => { callButton.disabled = status !== "connected"; });
This also covers reconnects — if the network drops mid-session, the client recovers on its own and fires connection_status again once it’s back.
Placing a call
Once connected, dialing is one call:
await client.call(contact.phoneNumber, contact.crmId, contact.name); contact.phoneNumberis dialed exactly as passed in — your CRM includes the country code if the destination needs one.contact.crmId(theexternalRefargument) is an opaque id echoed back untouched on every event — use it to look the call back up against your own records.contact.name(theleadNameargument) is display-only, shown in the built-in widget, or readable fromclient.getCurrentCall()in a custom UI.
client.call() rejects with a 403 rather than letting it ring. No extra compliance logic is needed on your end for this case. Reacting to what happens next
This is the part that trips people up: the agent’s own phone connecting is not the same as the customer answering. The SDK models this as two separate events:
client.on("connecting", (call) => { // Originated, nothing is ringing anywhere yet. }); client.on("ringing", (call) => { // The agent's own leg picked up — Belsmart is now dialing the CUSTOMER. // Don't start a call timer here. }); client.on("active", (call) => { // The customer answered. Start a duration timer or "on call" indicator here. }); client.on("ended", (call) => { console.log(`${call.toNumber} — ${call.duration}s`); // duration is measured from "active", not from when dialing started. });
A practical rule of thumb: drive your UI off active and ended, not connecting and ringing. The latter two are useful for a “calling…” state, but duration or outcome logic should key off active/ended.
Controlling a call while it’s live
Once a call is active, these are available immediately:
client.setMuted(true); await client.hold(); client.setVolume(0.5); // local playback only, 0–1 client.sendDtmf("1"); // e.g. navigating an IVR on the far end client.hangup();
If you’re using the built-in widget (createWidget(client)), all of this is already wired to buttons — you only need these calls if you’re building a custom UI instead.
Adding a third party (conference)
Once a call is active, adding someone else folds it into a conference — everyone can hear each other from that point on:
await client.addToConference(supervisorNumber, "Jane (Supervisor)");
To build a roster (who’s on the call, who’s muted, who’s on hold):
const participants = await client.listConferenceParticipants();
This has no push updates — call it again after any action to refresh. The built-in widget polls it automatically; a custom UI needs to do the same.
Ending it:
client.hangup(); // drops only your own leg await client.hangupAll(); // ends the call for everyone
Handing off a call (attended transfer)
Unlike a conference, an attended transfer lets you privately consult someone before deciding whether to connect them to the customer:
await client.startConsult(supervisorNumber); // customer is held, can't hear this conversation // then either: await client.completeTransfer(); // bridges customer + consult target, you drop off // or: await client.cancelConsult(); // drops consult target, resumes with customer
There’s no built-in widget UI for this yet — it’s a custom-UI-only feature today.
Recording an outcome
Dispositions are typically set after the call ends, once the agent has picked an outcome in your own UI:
client.on("ended", async (call) => { // show your outcome picker, then: await client.setDisposition(chosenOutcome.name); });
Putting it together: a minimal click-to-call button
Everything above, wired into one button.
import { BelsmartCrmClient } from "@belsmart/crm-sdk"; const client = new BelsmartCrmClient({ apiBaseUrl, apiKey, apiSecret, agentUuid }); await client.init(); client.on("connection_status", ({ status }) => { callButton.disabled = status !== "connected"; }); callButton.addEventListener("click", async () => { try { await client.call(contact.phoneNumber, contact.crmId, contact.name); } catch (err) { if (err.message?.includes("DNC")) { showToast("This number is on the Do Not Call list."); } else { showToast("Couldn't place the call. Try again."); } } }); client.on("active", (call) => startTimer(call.callId)); client.on("ended", (call) => { stopTimer(call.callId); showDispositionPicker(call); });