Introduction
The BelSmart API lets you build calling workflows on top of the same Tier 1 telecom network that powers the BelSmart dialer — trigger calls, read agent status, pull call records, and fetch recordings.
Base URL
All API requests are made against a single base URL:
https://api.belsmart.io/v1 Quick start
Every request needs your API key in the Authorization header. Here's a call to list your agents:
curl https://api.belsmart.io/v1/agents \
-H "Authorization: Bearer YOUR_API_KEY" Response format
All responses are JSON. List endpoints return a data array plus pagination fields; single-resource endpoints return the object directly. Errors return a 4xx/5xx status with a machine-readable code and a human-readable message.
Authentication
Requests are authenticated with an API key, sent as a bearer token. Generate keys from your BelSmart dashboard under Settings → API Keys.
Using your API key
Add the key to the Authorization header of every request:
curl https://api.belsmart.io/v1/calls \
-H "Authorization: Bearer YOUR_API_KEY" Key types
| Prefix | Environment | Notes |
|---|---|---|
| sk_live_ | Production | Real calls, billed to your account. |
| sk_test_ | Sandbox | Simulated call flow, no telecom charges. |
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.
agentUuid. 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: "YOUR_API_KEY",
apiSecret: "YOUR_API_SECRET",
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.
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.
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);
}); Agent List
Look up your agent roster and build the mapping between BelSmart agents and your own CRM UI.
import { BelsmartCrmClient } from "@belsmart/crm-sdk";
const agents = await BelsmartCrmClient.listAgents({
apiBaseUrl,
apiKey,
apiSecret
});
// [{ agentUuid, name, email, extension }, ...] Returned agent fields
| Field | Type | Description |
|---|---|---|
| agentUuid | string | Unique identifier for the agent. |
| name | string | Agent's display name. |
| string | Agent's account email. | |
| extension | string | Agent's internal dialing extension. |
Call Detail Records (CDR)
Fetches the gateway's own call detail record — numbers, timestamps, duration, hangup cause and disposition.
callId is optional — omit it to default to the most recently ended call, or pass one explicitly to look up an older call (e.g. one saved in your own DB). const cdr = await client.getCallCdr(callId);
// or client.getCallCdr() for the last ended call
// {
// callId,
// direction,
// fromNumber,
// toNumber,
// startedAt,
// answeredAt,
// endedAt,
// duration,
// billableDuration,
// hangupCause,
// disposition,
// externalRef
// } Returned CDR fields
| Field | Type | Description |
|---|---|---|
| callId | string | Unique identifier for the call. |
| direction | string | Call direction, e.g. inbound or outbound. |
| fromNumber | string | Originating number. |
| toNumber | string | Destination number. |
| startedAt | string | Timestamp the call was initiated. |
| answeredAt | string | Timestamp the call was answered, if it was. |
| endedAt | string | Timestamp the call ended. |
| duration | number | Total call length. |
| billableDuration | number | Portion of the call counted for billing. |
| hangupCause | string | Gateway-level reason the call ended. |
| disposition | string | Call outcome, e.g. connected, no answer, voicemail. |
| externalRef | string | Your own reference ID, if one was attached to the call. |
Recording
Fetches the recording's playback/download URL for a call.
null if the call wasn't recorded or the gateway hasn't finished processing it yet — worth a short retry/poll in that case. Same optional callId convention as getCallCdr(). const recording =
await client.getCallRecording(callId);
// or client.getCallRecording()
if (recording) {
// {
// callId,
// url,
// duration,
// format,
// availableAt
// }
console.log(
"play/download at:",
recording.url
);
} Returned recording fields
| Field | Type | Description |
|---|---|---|
| callId | string | The call this recording belongs to. |
| url | string | Playback/download URL for the recording. |
| duration | number | Length of the recording. |
| format | string | Audio format of the recording. |
| availableAt | string | Timestamp the recording became available. |