This is a script that uses Graph module to fetch and scan calls to find out a summary of calls for a specific user for the selected range

Since teams admin portal doesn’t give a proper tool for exporting those calls.

Depending on the size of the logs it can take around 20 min to run.(If you guys have a better way around please tell me)

PLEASE MAKE SURE TO REVISE BEFORE RUNNING!!!

You need :
An application registration
a certificate PFx for script and Cer for app
Permission needed -
CallRecords.Read.All (REQUIRED)

  • User.Read.All, Directory.Read.All (Recommended)

  • AuditLog.Read.All, Reports.Read.All (Optional)

Might need other permission…

here is the script to run

$tenantAdminUrl = “https://yourcompany-admin.sharepoint.com
$clientId = “appID/ClientID”
$tenantId = “tenantID”
$CertThumbprint = “ceRt for identification”
$url = “https://yourcompany.sharepoint.com/

Connect-MgGraph -ClientId $clientId -CertificateThumbprint $CertThumbprint -TenantId $tenantId -NoWelcome

function Get-TeamsCallAuditByUser {
param (
[Parameter(Mandatory = $true)]
[string]$UserObjectId,

    [Parameter(Mandatory = $true)]
    [datetime]$StartDate,

    [Parameter(Mandatory = $true)]
    [datetime]$EndDate
)

# ----------------------------------------
# Required Graph Permissions Reminder
# ----------------------------------------
Write-Warning @"

This function requires Microsoft Graph application permissions:

  • CallRecords.Read.All (REQUIRED)
  • User.Read.All, Directory.Read.All (Recommended)
  • AuditLog.Read.All, Reports.Read.All (Optional)

Ensure these are granted and consented in Entra ID > App Registrations > API Permissions.
To remove this notice, comment or delete this block.
"@

# ----------------------------------------
# Configuration
# ----------------------------------------
$ThrottleDelay = 15
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = "C:\temp\CallLog-$($UserObjectId)-$timestamp.csv"

$DaysToScan = ($EndDate - $StartDate).Days
if ($DaysToScan -lt 1) { $DaysToScan = 1 }

$MaxTotalRecordsToScan = $DaysToScan * 6000

$matchingCallIds = @()
$callDetails = @()
$pageCounter = 0
$totalMatchingCalls = 0
$allFetchedRecords = 0
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# ----------------------------------------
# Fetch Call Records
# ----------------------------------------
$uri = "https://graph.microsoft.com/v1.0/communications/callRecords?\$orderby=startDateTime desc"

do {
    $response = Invoke-MgGraphRequest -Method GET -Uri $uri
    $pageCounter++
    $records = $response.value

    foreach ($record in $records) {
        $allFetchedRecords++
    
        if ($allFetchedRecords -ge $MaxTotalRecordsToScan) {
            Write-Host "Reached scan limit: $MaxTotalRecordsToScan records." -ForegroundColor Yellow
            $uri = $null
            break
        }
    
        $recordStart = [datetime]$record.startDateTime
        if ($recordStart -lt $StartDate -or $recordStart -ge $EndDate) {
            continue
        }
    
        $json = $record | ConvertTo-Json -Depth 5 | ConvertFrom-Json
        $found = $false
    
        if ($json.participants -ne $null) {
            foreach ($p in $json.participants) {
                if ($p -ne $null -and $p.user -ne $null -and $p.user.id -eq $UserObjectId) {
                    $found = $true
                    break
                }
            }
        }
    
        if (
            ($json.organizer -ne $null -and $json.organizer.user.id -eq $UserObjectId) -or
            ($json.organizer_v2 -ne $null -and $json.organizer_v2.identity.user.id -eq $UserObjectId)
        ) {
            $found = $true
        }
    
        if ($found) {
            $matchingCallIds += $json.id
            $totalMatchingCalls++
            Write-Host "Matched callId: $($json.id)" -ForegroundColor Green
        }
    }

    Write-Host "Page $pageCounter complete. Matches so far: $totalMatchingCalls. Records scanned: $allFetchedRecords."
    $uri = $response.'@odata.nextLink'
    Start-Sleep -Milliseconds $ThrottleDelay

} while ($null -ne $uri)

Write-Host "Total matches: $totalMatchingCalls" -ForegroundColor "green"

# ----------------------------------------
# Fetch Call Details
# ----------------------------------------
$callCounter = 0
foreach ($callId in $matchingCallIds) {
    try {
        $detail = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/communications/callRecords/$callId"
        $callCounter++

        $startTime = $detail.startDateTime
        $endTime = $detail.endDateTime
        $duration = [math]::Round(([datetime]$endTime - [datetime]$startTime).TotalMinutes, 2)

        $participants = ($detail.participants |
            ForEach-Object { $_.user.displayName } |
            Where-Object { $_ } |
            Sort-Object |
            Get-Unique ) -join ', '

       $callDetails += [PSCustomObject]@{
            CallId       = $callId
            StartTime    = $startTime
            EndTime      = $endTime
            DurationMin  = $duration
            Participants = $participants
            Type         = $detail.type
        }

        if ($callCounter % 50 -eq 0) {
            Write-Host "Processed $callCounter calls..."
        }

        Start-Sleep -Milliseconds $ThrottleDelay
    }
    catch {
        Write-Warning "Failed to fetch details for callId $callId : $_"
    }
}

$callDetails | Export-Csv -Path $OutputPath -Encoding UTF8 -NoTypeInformation
$sw.Stop()

Write-Host "Export complete: $($callDetails.Count) calls saved to $OutputPath"
Write-Host "Total runtime: $([math]::Round($sw.Elapsed.TotalMinutes, 2)) minutes."

}

Get-TeamsCallAuditByUser -UserObjectId “Get the userobject ID on Teamsadmin portal” -StartDate “2025-04-07” -EndDate “2025-05-07”

3 Spice ups

You can also change the max records to scan! I put 6000 per day since my company usually make around 6000 calls a day but if its more (can be viewed via the teams admin portal) feel free to change it!

2 Spice ups

Btw save path is in temp for easier usage purposes please change it!

1 Spice up

Just writing a status output every 50 calls so you know it’s not hung?

1 Spice up

(post deleted by author)

yes exactly it’s just to have a visual and since MGgraph api throttles you. its nice to see that it is still going.

1 Spice up