⚡ Calls API

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.

Before you start: everything below assumes setup is already done — your tenant has issued an API key/secret pair, and you’ve mapped the signed-in CRM user to a Belsmart agentUuid. See the reference page’s setup steps if you haven’t done this part yet.

What you can do with it

Place a callTo a contact’s number with one function call.
React to the lifecycleRinging, answered, ended — via events, not polling.
Control a live callMute, hold, send DTMF, adjust volume.
Bring in a third partySupervisor listen-ins, warm intros, ad-hoc conferences.
Hand off a callAttended transfer — without ending it for the customer.
Record what happenedAttach a disposition once the call ends.

1

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.

2

Placing a call

Once connected, dialing is one call:

await client.call(contact.phoneNumber, contact.crmId, contact.name);
  • contact.phoneNumber is dialed exactly as passed in — your CRM includes the country code if the destination needs one.
  • contact.crmId (the externalRef argument) is an opaque id echoed back untouched on every event — use it to look the call back up against your own records.
  • contact.name (the leadName argument) is display-only, shown in the built-in widget, or readable from client.getCurrentCall() in a custom UI.
If the number is on the tenant’s Do Not Call list, the call is blocked server-side before anything is dialed — client.call() rejects with a 403 rather than letting it ring. No extra compliance logic is needed on your end for this case.
3

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.

4

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.

5

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
6

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.

7

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);
});
Worth knowing: this currently writes to Belsmart’s partner-call tracking record only — it’s not yet reflected in Belsmart’s own CDR or reporting dashboards. Treat it as data your integration can read back, not something that changes Belsmart’s own reports today.

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);
});
Need a hand?

Wiring this into your CRM?

Our integrations team can walk through your setup and mapping live — most integrations are working end to end within a day.

Talk to Integrations →
Or email us directly at sales@belsmart.io — we reply within one business day.
© 2026 Belsmart. All Rights Reserved.