A colleague of mine, Nico De Cleyre, recently was struggling with adding a Notebook-tab to a Team in his Microsoft Teams provisioning script.

As Microsoft mentions Notebook tabs in their documentation, they also outline that configuration for these kind of tabs are not supported by default.

But… Nico wouldn’t be Nico if he didn’t find a solution to this. So we’d love to share this with you. We start by explaining the difficulty, to end with a working Powershell script to add a Notebook tab to your Team.

Okay, let’s get started!

The hard part is figuring out the URl to a notebook. When you want to add a tab to a channel with Microsoft Graph, the API despises a body you can find in this documentation. Some elements highlighted:

  • teamsApp@odata.bind: always of the type: “https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{Teams App ID}
    • With the Teams App ID for a OneNote always being 0d820ecd-def2-4297-adad-78056cde7c78
  • WebUrl: https://www.onenote.com/teams/TabRedirect?redirectUrl={url to OneNote}
  • ContentUrl en RemoveUrl: both the same, with the only distinction being the URL for ContentUrl containing “TabContent” and the URL for RemoveUrl “TabRemove”.

Because ContentUrl and RemoveURL are very long, you quickly run into the following error message: “Configuration size exceeded. Provided:’2350’bytes MaxAllowed:’4096’bytes”. Luckily you can get rid of the parameters webUrl and RemoveUrl, we only need the ContentUrl-parameter.

But how exactly do you form this ContentUrl?

The ContentUrl looks as follows:

https://www.onenote.com/teams/TabContent?
notebookSource=Pick
&notebookSelfUrl=https://www.onenote.com/api/v1.0/myOrganization/siteCollections/{Site Collection ID}/sites/{Site ID}/notes/notebooks/{NoteBook ID}
&oneNoteWebUrl={Url to the notebook}
&notebookName={Name of the notebook}
&siteUrl={URL of the site}
&createdTeamType=Standard
&oneNoteClientUrl={Client url of the notebook, which in fact is the same as the oneNoteWebUrl}
&notebookIsDefault={IsDefault: True or false}
&ui={locale}
&tenantId={tid}
&upn={userPrincipalName}
&groupId={groupId}
&theme={theme}
&entityId={entityId}
&subEntityId={subEntityId}
&sessionId={sessionId}
&ringId={ringId}
&teamSiteUrl={teamSiteUrl}
&channelType={channelType}
&trackingId={appSessionId}
&hostClientType={hostClientType}

Note that the items between braces that are not in bold should not be provided, these are placeholders that MS Teams fill in themselves.

To discover the above bold parameters, you need 3 Graph calls.

  1. https://graph.microsoft.com/v1.0/groups/{group ID}/sites/root
    The result of this call provides a string with 3 values:   
    1. Site collection hostname (we don’t need this for this solution)
    2. Site collection ID (GUID)
    3. Site ID (GUID)
  2. https://graph.microsoft.com/v1.0/groups/{group ID}/onenote/notebooks
    The call to create a notebook within the Office 365 group (Team). The response results in:   
    1. a self url which we need in the following call
    2. ID of the notebook
  3. Self url from previous response (https://graph.microsoft.com/v1.0/groups/{group ID}/onenote/notebooks/{notebook ID})
       With this call we gather additional information about our notebook:
    1. The IsDefault value
    2. The weburl of the notebook
    3. The name of the notebook

Let’s put it all together.

To authenticate we use an access token based on Azure App registration ID & Secret.

function Create-OneNoteTab(){
    [cmdletbinding()]
	param
	(
		[Parameter(Mandatory = $True)]
		[String]$ChannelID,
		[Parameter(Mandatory = $True)]
		[string]$GroupID,
		[Parameter(Mandatory = $True)]
		[string]$Name
    )
    try
	{
        $headers = @{
            'Content-Type'  = 'application/json'
            'Authorization' = 'Bearer ' + $($global:accessToken)
        }

        $createNotebookUri = "https://graph.microsoft.com/v1.0/groups/$GroupID/onenote/notebooks"

        $createNotebookBody = @"
        {
            "displayName": "$Name"
        }
"@

        $createNotebookResponse = Invoke-RestMethod -Method Post -Headers $headers -Body $createNotebookBody -Uri $createNotebookUri

        $notebookId = $createNotebookResponse.id
        $selfUri = $createNotebookResponse.self

        Write-Host "Id: $id, selfUri: $selfUri"

        $notebookResponse = Invoke-RestMethod -Headers $headers -Method Get -Uri $selfUri
        
        $isDefault = $notebookResponse.isDefault
        $notebookUrl = $notebookResponse.links.oneNoteWebUrl.href
        #$notebookClientUrl = $notebookResponse.links.oneNoteClientUrl.href
        $name = $notebookResponse.displayName

        $siteUri = "https://graph.microsoft.com/v1.0/groups/$GroupID/sites/root"
        $siteResponse = Invoke-RestMethod -Method Get -Headers $headers -Uri $siteUri

        $siteUrl = $siteResponse.webUrl
        $splittedSiteResponseId = $siteResponse.id.Split(",")

        $sitecollectionId = $splittedSiteResponseId[1]
        $siteId = $splittedSiteResponseId[2]

        $notebookTabUri = "https://graph.microsoft.com/v1.0/teams/$GroupID/channels/$ChannelID/tabs"

        $appendix = "notebookSource=Pick&notebookSelfUrl=https://www.onenote.com/api/v1.0/myOrganization/siteCollections/$sitecollectionId/sites/$siteId/notes/notebooks/$notebookId&oneNoteWebUrl=$notebookUrl&notebookName=$([uri]::EscapeUriString($name))&siteUrl=$siteUrl&createdTeamType=Standard&oneNoteClientUrl=$notebookUrl&notebookIsDefault=$($isDefault.ToString().ToLower())&ui={locale}&tenantId={tid}&upn={userPrincipalName}&groupId={groupId}&theme={theme}&entityId={entityId}&subEntityId={subEntityId}&sessionId={sessionId}&ringId={ringId}&teamSiteUrl={teamSiteUrl}&channelType={channelType}&trackingId={appSessionId}&hostClientType={hostClientType}"
    
        $notebookBody = @"
            {
                "displayName": "$name",
                "teamsApp@odata.bind": "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/0d820ecd-def2-4297-adad-78056cde7c78",
                "configuration": {
                    "entityId": "",
                    "contentUrl": "https://www.onenote.com/teams/TabContent?$appendix",
                    "removeUrl": null,
                    "websiteUrl": null
                }
            }
"@

        $notebookTabResponse = Invoke-RestMethod -Method Post -Headers $headers -body $notebookBody -Uri $notebookTabUri

        Write-Host "Tab created $notebookUrl"

    }catch
	{
        Write-Error "$($_ | Format-List | Out-String)"
        break
	}

} 

We hope we can save you some time with this solution.