Objective
Learn how to reconcile Twilio Programmable Voice conference participant mixing charges for a specific phone number or endpoint over a target billing period. Use this guide to audit conference participant minutes and isolate mixing fees associated with a particular caller using Node.js and Twilio REST APIs.
Product
- Programmable Voice
User Account Permission/Role(s) Required
- Administrator, Developer, or Billing role with access to Twilio Account SID and Auth Token.
Procedure
Twilio Conference participant mixing is billed per participant leg at a standard rate (such as $0.0018 per minute for US conferences), rounded up to the nearest full minute. To calculate the total mixing cost incurred by conferences involving a specific phone number or client identifier, follow the steps below.
Prerequisites
- Have NodeJS installed
- Have the Twilio nodejs helper library installed
Step 1: Implement the Reconciliation Script
Create a script named reconcile_conference_charges.js and insert the following code:
import twilio from "twilio";
// ==========================================
// CONFIGURATION
// ==========================================
const TARGET_MONTH = 11;
const TARGET_YEAR = 2026;
const TARGET_CALLER = "+18778894546";
const CONFERENCE_PARTICIPANT_RATE = 0.0018;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
function getMonthBoundaries(year, month) {
const start = new Date(Date.UTC(year, month - 1, 1, 0, 0, 0));
const end = new Date(Date.UTC(year, month, 1, 0, 0, 0));
return { start, end };
}
function normalizeNumber(num) {
return (num || "").replace(/\D/g, "");
}
function numbersMatch(num1, num2) {
const n1 = normalizeNumber(num1);
const n2 = normalizeNumber(num2);
if (!n1 || !n2) return false;
return n1 === n2 || n1.endsWith(n2) || n2.endsWith(n1);
}
/**
* Filters out SIP client setup legs and intermediate dial legs.
* In a 3-leg SIP-to-Twilio-to-Twilio flow:
* Leg 1 (SIP -> Twilio) has children. (Discard: parent routing leg)
* Leg 2 (Twilio -> Twilio) has parentCallSid = Leg 1. (Discard: outbound dial leg)
* Leg 3 (Twilio receives -> Conference) is inbound, has no child legs. (KEEP)
*/
function isolateConferenceParticipantLegs(candidateCalls, allMonthCalls) {
// Build a set of all calls that acted as parents (e.g. Leg 1)
const parentCallSids = new Set(
allMonthCalls
.map((c) => c.parentCallSid)
.filter((sid) => sid !== null && sid !== undefined)
);
return candidateCalls.filter((call) => {
// 1. Exclude outbound dial/API legs (Leg 2)
// When Twilio dials out to another Twilio number via <Dial>, it sets parentCallSid
// and direction is outbound-dial/outbound-api.
if (call.parentCallSid) {
return false;
}
// 2. Exclude parent calls that spawned a child leg (Leg 1)
// The SIP leg that dialed the conference host spawns the outbound leg
if (parentCallSids.has(call.sid)) {
return false;
}
// 3. Exclude raw SIP / Client endpoints acting as the carrier bridge
if ((call.from || "").startsWith("sip:") || (call.from || "").startsWith("client:")) {
return false;
}
return true;
});
}
(async () => {
try {
const { start, end } = getMonthBoundaries(TARGET_YEAR, TARGET_MONTH);
console.log(`\nAnalyzing Historical Month: ${TARGET_YEAR}-${String(TARGET_MONTH).padStart(2, "0")}`);
console.log(`Range: ${start.toISOString()} -> ${end.toISOString()}`);
console.log(`Target Participant: ${TARGET_CALLER}`);
console.log("Fetching historical conference sessions & calls...");
// 1. Fetch completed conferences
const conferences = await client.conferences.list({
status: "completed",
dateCreatedAfter: start,
dateCreatedBefore: end,
});
console.log(`Found ${conferences.length} completed conferences in this month.`);
// 2. Fetch all completed calls across the account for that month
const allCalls = await client.calls.list({
startTimeAfter: start,
startTimeBefore: end,
status: "completed",
});
console.log(`Found ${allCalls.length} total call records.`);
let grandTotalBilledMinutes = 0;
let grandTotalCost = 0.0;
const matchedConferences = [];
// 3. Correlate calls to conferences
for (const conf of conferences) {
const confStart = new Date(conf.dateCreated).getTime();
const confEnd = new Date(conf.dateUpdated).getTime();
// Find all calls overlapping this conference session
const overlappingCalls = allCalls.filter((call) => {
const callStart = new Date(call.startTime).getTime();
const durationMs = parseInt(call.duration || "0", 10) * 1000;
const callEnd = callStart + durationMs;
return callStart <= confEnd + 5000 && callEnd >= confStart - 5000;
});
// Filter down to ONLY the terminal leg that actually ran the conference
const trueParticipants = isolateConferenceParticipantLegs(overlappingCalls, allCalls);
// Verify the target participant was present in this room
const targetPresent = trueParticipants.some(
(call) => numbersMatch(call.from, TARGET_CALLER) || numbersMatch(call.to, TARGET_CALLER)
);
if (!targetPresent) continue;
let confBilledMinutes = 0;
const participantBreakdown = [];
for (const call of trueParticipants) {
const durationSec = parseInt(call.duration || "0", 10);
const billedMins = durationSec > 0 ? Math.ceil(durationSec / 60) : 0;
const legCost = billedMins * CONFERENCE_PARTICIPANT_RATE;
confBilledMinutes += billedMins;
const isTarget = numbersMatch(call.from, TARGET_CALLER) || numbersMatch(call.to, TARGET_CALLER);
const callerNumber = call.from || call.to || "Unknown";
participantBreakdown.push({
callSid: call.sid,
callerNumber,
direction: call.direction,
durationSeconds: durationSec,
billedMinutes: billedMins,
cost: legCost,
isTarget,
});
}
const confCost = confBilledMinutes * CONFERENCE_PARTICIPANT_RATE;
grandTotalBilledMinutes += confBilledMinutes;
grandTotalCost += confCost;
matchedConferences.push({
conferenceSid: conf.sid,
friendlyName: conf.friendlyName,
dateCreated: conf.dateCreated,
participantCount: trueParticipants.length,
billedMinutes: confBilledMinutes,
cost: confCost,
participants: participantBreakdown,
});
}
// 4. Output Summary Report
console.log("\n" + "=".repeat(65));
console.log(`HISTORICAL CONFERENCE REPORT (ISOLATED CONFERENCE LEGS)`);
console.log(`Month: ${TARGET_YEAR}-${String(TARGET_MONTH).padStart(2, "0")}`);
console.log(`Target Number: ${TARGET_CALLER}`);
console.log(`Conferences Attended: ${matchedConferences.length}`);
console.log(`Total Participant Leg Minutes: ${grandTotalBilledMinutes} mins`);
console.log(`Total Participant Mixing Cost: $${grandTotalCost.toFixed(4)}`);
console.log(`(Rate: $${CONFERENCE_PARTICIPANT_RATE}/min per participant)`);
console.log("=".repeat(65));
for (const conf of matchedConferences) {
console.log(`\nConference: ${conf.friendlyName} (${conf.conferenceSid})`);
console.log(` Date: ${conf.dateCreated}`);
console.log(` Participants: ${conf.participantCount}`);
console.log(` Participant Billed Mins: ${conf.billedMinutes} mins | Cost: $${conf.cost.toFixed(4)}`);
for (const p of conf.participants) {
const marker = p.isTarget ? " [TARGET]" : "";
console.log(` - Number: ${p.callerNumber} | CallSid: ${p.callSid} (${p.direction}) | ${p.durationSeconds}s (${p.billedMinutes}m billed) | $${p.cost.toFixed(4)}${marker}`);
}
}
} catch (err) {
console.error("Execution failed:", err);
}
})();Step 2: Update the configuration values:
// ==========================================
// CONFIGURATION
// ==========================================
const TARGET_MONTH = 11;
const TARGET_YEAR = 2026;
const TARGET_CALLER = "+18778894546";Step 3: Run the script
You will see an output like the following:
Selected Window: 2026-11
Range: 2026-11-01T00:00:00.000Z -> 2026-11-11T05:49:02.753Z
Using Voice Insights API (recent month query)...
Found 3 candidate conferences to inspect.
============================================================
CONFERENCE PARTICIPANT MIXING COST REPORT
Month: 2026-11
Target: +18778894546
Conferences Attended: 3
Total Participant Leg Minutes: 103 mins
Participant Mixing Cost: $0.1854
(Rate: $0.0018/min per participant)
============================================================
Conference: conf1 (CFb4b84720b5c7ff8c01029920cbd0b21d)
Date: Tue Nov 10 2026 14:36:48 GMT-0800 (Pacific Standard Time)
Participants: 2
Total Room Billed Mins: 12 mins | Cost: $0.0216
- Caller: +18778894546 | CallSid: CA8901d418f199920ecc84c372dede7959 | 332s (6m billed) | $0.0108 [TARGET]
- Caller: +1650******* | CallSid: CA1ac5cf8a8f938067cc02b7d6e3063660 | 325s (6m billed) | $0.0108
Conference: conf1 (CF98d9de875c401549d04acaf6d8d02f4a)
Date: Tue Nov 10 2026 13:54:46 GMT-0800 (Pacific Standard Time)
Participants: 4
Total Room Billed Mins: 83 mins | Cost: $0.1494
- Caller: +18778894546 | CallSid: CA89cbeb5acaefe4373650f08eba31e0c1 | 2180s (37m billed) | $0.0666 [TARGET]
- Caller: +1925******* | CallSid: CA850207cfbc0f891dd546d658241b40b6 | 1566s (27m billed) | $0.0486
- Caller: +1650******* | CallSid: CAd6fd37e4eb492b539aa6be44640aa8be | 743s (13m billed) | $0.0234
- Caller: +1925******* | CallSid: CA591531c2b4458d225b168a0f21a3edd5 | 336s (6m billed) | $0.0108
Conference: conf1 (CFef312a3d84cb1b295c5ccfddd4087182)
Date: Fri Nov 06 2026 23:43:33 GMT-0800 (Pacific Standard Time)
Participants: 2
Total Room Billed Mins: 8 mins | Cost: $0.0144
- Caller: +18778894546 | CallSid: CAc532c9ffd8169cf7811564be337e344f | 212s (4m billed) | $0.0072 [TARGET]
- Caller: +1925******* | CallSid: CAa56ba51e7a065c1296d32c34594b331d | 188s (4m billed) | $0.0072Additional Information
Participant Mixing vs. Inbound/Outbound Minutes: Participant mixing charges are separate from standard inbound or outbound PSTN carrier minute rates. Both charges apply when participants connect via standard telephone numbers.
Minute Rounding: Twilio rounds partial minutes up to the nearest whole minute for each participant leg independently.