Did you ever have to provision Teams with one or more private channels prior to a data migration? You might have encountered an issue that the private channel SharePoint Online sites are not provisioned automatically. This post outlines this issue and the solution I found to resolve the channel site provisioning.

So what I encountered was, that when provisioning private channels with PowerShell, the channel was created, but the underyling SharePoint Online site for that channel was not. So by using something as the following in a loop:

Add-PnPTeamsChannel -Team MyTeam -DisplayName "My Channel" -Private -OwnerUPN user1@conotoso.com

When browsing for a solution, I noticed that a lot of people were running into this problem. Apparently it’s required to click on the “Files” tab for a private channel manually to request the provisioning for the private channel SharePoint Online site. Obviously that is a problem when you need to provision e.g. a 100 Teams with 2 private channels each.

PnP PowerShell to the resque as always and leveraging the power of the Graph to enable programmatic provisioning of private channel sites.

App registration

As a prerequisite you need to do an app registration in Azure AD with the appropriate Graph permissions allowing you to do what you need to do.

The following snippet provides the basics of registering the app, using certificate based authentication to get a token later on, and set the permissions for the app in Azure AD.

#Session variables
$orgName = 'Contoso'
$appRegName = 'ContosoProvisioning'
$graphPermissions = "Group.Read.All", `
"Group.ReadWrite.All", `
"Directory.Read.All", `
"Directory.ReadWrite.All", `
"Channel.ReadBasic.All", `
"ChannelSettings.Read.All", `
"ChannelSettings.ReadWrite.All", `
"Channel.Create", `
"Team.ReadBasic.All", `
"TeamSettings.Read.All", `

#Initialize the app registration
$app = Register-PnPAzureADApp  `
-ApplicationName $appRegName `
-Tenant "$($orgName).onmicrosoft.com" `
-GraphApplicationPermissions $graphPermissions  `
-Store 'CurrentUser'  `
#Store the app credentials in the Windows credential store
$thumbprint = ConvertTo-SecureString $app.'Certificate Thumbprint' -AsPlainText -Force

Add-PnPStoredCredential -Name $appRegName  -Username $app.'AzureAppId/ClientId'  -Password $thumbprint -Overwrite

The script stores the certificate in the local certificate store on the Windows machine, so you’ll need to run the script as an administrator. The credentials contain the client id for the app as the username and the thumbprint for the certificate as the password. We’ll need these in the next step, but for now, these are stored in the Windows Credential manager through the last step in the step.

Channel site provisioning

In order to provisioning the SharePoint Online channel site (TEAMCHANNEL#0), we need a Graph call that basically ‘visits’ the filesfolder for the private channel.

The following script gets a token from the Graph, then gets a list of Teams and private channels within and then perfom the ‘visit’ for the primary and private channels.

We’ll asume a small list without introducing paging in the script, to keep things simple.

#Session variables
$orgName = 'Contoso'
$appRegName = 'ContosoProvisioning'

#Get the stored credentials from the Windows Credential Store
$credential = Get-PnPStoredCredential -Name $appRegName -Type 'PSCredential'

#Connect to SharePoint Online using clientApp and certificate credentials
Connect-PnPOnline -Url "https://$($orgName)-admin.sharepoint.com" -ClientId $credential.UserName -Thumbprint $credential.GetNetworkCredential().Password -Tenant "$dstTenantName.onmicrosoft.com" -ReturnConnection

#Request graph access toeken
$accessToken = Get-PnPGraphAccessToken

#Get teams data via the Graph
$response = Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken" } -Uri 'https://graph.microsoft.com/beta/teams' -Method 'GET' -ContentType 'application/json'

#Select the data for each team
$teams = $response.value | Select-Object 'displayName', 'id'

#Loop through each team
foreach ($team in $teams) {

    #Capture critical errors on rowlevel
    try {

        #Get the primary channel
        $primaryChannel = Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken" } -Uri "https://graph.microsoft.com/beta/teams/$($team.id)/primaryChannel" -Method 'GET' -ContentType 'application/json' | Select-Object 'displayName', 'id'
        $privateChannels = (Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken" } -Uri "https://graph.microsoft.com/beta/teams/$($team.id)/channels?`$filter=membershipType eq `'private`'" -Method 'GET' -ContentType 'application/json').value | Select-Object 'displayName', 'id'

        #Trigger private channel SharePoint Onlinesite creation
        Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken" } -Uri "https://graph.microsoft.com/beta/teams/$($team.id)/channels/$($primaryChannel.id)/filesFolder" | Out-Null

        foreach ($privateChannel in $privateChannels) {

            #Attempt private channel check
            $stopLoop = $false
            [int]$retryCount = '0'

            do {
                try {
                    Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken" } -Uri "https://graph.microsoft.com/beta/teams/$($team.id)/channels/$($privateChannel.id)/filesFolder"
                    $stopLoop = $true
                catch {
                    if ($retryCount -gt $maxRetryCount) {
                        $stoploop = $true
                    else {

                        Start-Sleep -Seconds 5
                        $retryCount = $retryCount + 1
            While ($stopLoop -eq $false)


    catch {

        Write-Host $_



After completion, you should verify if the channel sites are indeed provisioned. The script does have some retry logic and pausing to handle wait times when performing the ‘visit’.

Further development

Please note that this script was created rather quickly to fulfil a specific requirement and may need some adjustments to e.g. adapt to a larger scale, or using an input file to scope the provisioning.

But perhaps it can be a good starter to include in your migration preparation activities.