All scripts
Microsoft 365 715

Teams Meeting Attendance Report

Pulls the attendance report for a specific Teams meeting using Microsoft Graph, including join and leave times for each participant, and exports it to CSV.

Get-TeamsMeetingAttendanceReport.ps1
<#
.SYNOPSIS
    Exports the attendance report for a Microsoft Teams meeting to CSV.

.DESCRIPTION
    Uses Microsoft Graph to find a Teams meeting by its join URL for a given
    organizer, then pulls the attendance report for that meeting, including
    each participant's join time, leave time, and total duration. Results
    are exported to a CSV file so you can hand them to whoever asked for
    proof of attendance, a training log, or a billing record.

    Requires the Microsoft.Graph module and delegated or application
    permissions for OnlineMeetings.Read.All and OnlineMeetingArtifact.Read.All.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    .\Get-TeamsMeetingAttendanceReport.ps1 -OrganizerUserId "jane.doe@contoso.com" -JoinUrl "https://teams.microsoft.com/l/meetup-join/..." -OutputPath "C:\Reports\attendance.csv"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$OrganizerUserId,

    [Parameter(Mandatory = $true)]
    [string]$JoinUrl,

    [Parameter(Mandatory = $false)]
    [string]$OutputPath = ".\TeamsMeetingAttendance.csv"
)

if (-not (Get-MgContext)) {
    Connect-MgGraph -Scopes "OnlineMeetings.Read.All", "OnlineMeetingArtifact.Read.All"
}

Write-Host "Looking up meeting for organizer $OrganizerUserId..."

$meetingUri = "https://graph.microsoft.com/v1.0/users/$OrganizerUserId/onlineMeetings?`$filter=JoinWebUrl eq '$JoinUrl'"
$meeting = Invoke-MgGraphRequest -Method GET -Uri $meetingUri

if (-not $meeting.value -or $meeting.value.Count -eq 0) {
    Write-Warning "No meeting found for that organizer and join URL. Double check both values."
    return
}

$meetingId = $meeting.value[0].id
Write-Host "Found meeting $meetingId. Pulling attendance reports..."

$reportsUri = "https://graph.microsoft.com/v1.0/users/$OrganizerUserId/onlineMeetings/$meetingId/attendanceReports"
$reports = Invoke-MgGraphRequest -Method GET -Uri $reportsUri

if (-not $reports.value -or $reports.value.Count -eq 0) {
    Write-Warning "No attendance reports exist for this meeting yet. Reports are usually available a few minutes after the meeting ends."
    return
}

$latestReportId = ($reports.value | Sort-Object -Property meetingEndDateTime -Descending)[0].id
$recordsUri = "https://graph.microsoft.com/v1.0/users/$OrganizerUserId/onlineMeetings/$meetingId/attendanceReports/$latestReportId/attendanceRecords"
$records = Invoke-MgGraphRequest -Method GET -Uri $recordsUri

$rows = foreach ($record in $records.value) {
    foreach ($interval in $record.attendanceIntervals) {
        [PSCustomObject]@{
            DisplayName = $record.identity.displayName
            Email       = $record.emailAddress
            Role        = $record.role
            JoinTime    = $interval.joinDateTime
            LeaveTime   = $interval.leaveDateTime
            DurationMin = [math]::Round($interval.durationInSeconds / 60, 1)
        }
    }
}

$rows | Sort-Object DisplayName, JoinTime | Export-Csv -Path $OutputPath -NoTypeInformation

Write-Host "Attendance report saved to $OutputPath ($($rows.Count) rows)."

Read it before you run it, and test in a safe tenant first.