SUPPORT.TWILIO.COM END OF LIFE NOTICE: This site, support.twilio.com, is scheduled to go End of Life on February 27, 2024. All Twilio Support content has been migrated to help.twilio.com, where you can continue to find helpful Support articles, API docs, and Twilio blog content, and escalate your issues to our Support team. We encourage you to update your bookmarks and begin using the new site today for all your Twilio Support needs.

How to Reconcile Twilio Conference Participant Mixing Charges by Phone Number or Endpoint

 

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.0072

Additional 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.

  • Conference Pricing

 

Have more questions? Submit a request
Powered by Zendesk