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”