Forum Discussion

LarsKnoll's avatar
LarsKnoll
Copper Contributor
Jul 27, 2026

TeamsTab.ReadWriteSelfForTeam: deleting a tab fails with 400 "appId cannot be null or whitespace"

We have a published Teams app that pins a configurable tab into a team channel via Microsoft Graph and removes it again when the user uninstalls our app from that channel. Creating the tab works. Deleting it always fails with a 400 that looks like an unhandled null-argument exception inside the Graph Teams service rather than a permission problem.

I reported this in October 2025 as https://github.com/MicrosoftDocs/msteams-docs/issues/13432, but that is a documentation repository and the issue has had no response, so I am reposting here, as we can still reproduce the issue.

We're using a delegated token for the operation. The only tab-related scopes are `TeamsTab.Read.All` (read-only) and `TeamsTab.ReadWriteSelfForTeam`. According to the docs (https://learn.microsoft.com/en-us/graph/api/channel-delete-tabs), `TeamsTab.ReadWriteSelfForTeam` is the **least privileged** delegated permission for the delete, so the token should be ok.

What works

Creating the tab, with the same delegated user token:

POST https://graph.microsoft.com/v1.0/teams/«teamId»/channels/«channelId»/tabs
Content-Type: application/json

{
  "displayName": "«AppName»",
  "email address removed for privacy reasons": "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/«teamsAppId»",
  "configuration": {
    "entityId": "«entityId»",
    "contentUrl": "https://«ourhost»/teams/tabs/channel/«entityId»",
    "websiteUrl": "https://«ourhost»/app/tickets?channel=«entityId»",
    "removeUrl": "https://«ourhost»/teams/remove-tab/«entityId»"
  }
}

→ `201 Created`. The tab appears in the channel and renders correctly.

 

Listing the tabs afterwards also works and returns our tab together with its `id`:

GET https://graph.microsoft.com/v1.0/teams/«teamId»/channels/«channelId»/tabs?$expand=teamsApp

Works as well and lists our tab. Might be served by the TeamsTab.Read.All permission.

What fails

DELETE https://graph.microsoft.com/v1.0/teams/«teamId»/channels/«channelId»/tabs/«tabId»

→ `400 Bad Request`

{
    "error":{
        "code":"BadRequest",
        "message":"appId cannot be null or whitespace. (Parameter 'appId')",
        "innerError":{
            "date":"2026-07-27T13:45:31",
            "request-id":"f930c847-0b48-4a46-b17e-f0ee10ff8177",
            "client-request-id":"f930c847-0b48-4a46-b17e-f0ee10ff8177"
        }
    }
}

(leaving in the error details, maybe that helps diagnosing the issue)

appID is not even part of the request, it's probably resolved against the TeamsTab.ReadWriteSelfForTeam permission on your side.

Let me know if you need any more details.

2 Replies

  • Hello LarsKnoll,
    We are tried with the above scenario and it is working fine from our end.
    I'd suggest verifying that the tab was created by the same Teams app/App ID used in the access token, confirming the token contains the expected TeamsTab.ReadWriteSelfForTeam scope, and comparing the teamsAppId returned by GET .../tabs?$expand=teamsApp with the app identity in the token.

     

  • Your DELETE request and delegated scope match Microsoft’s documented channel-tab API: TeamsTab.ReadWriteSelfForTeam is the least-privileged delegated permission, the call has no body, and success returns 204. That scope only lets a Teams app manage its own tabs, so the 400 indicates Graph cannot resolve the calling app’s identity to the tab’s teamsApp relationship; an appId does not belong in the DELETE request.

     

    Decode the access token and confirm aud is Microsoft Graph, scp includes TeamsTab.ReadWriteSelfForTeam, and azp/appid is the Entra application used by the package. Then GET the tab with $expand=teamsApp and compare teamsApp.externalId with the Teams manifest id; the catalog teamsApp.id can differ for organizational or sideloaded apps. Re-upload the current package and recreate the tab if those identities differ. As a diagnostic, test with TeamsTab.ReadWriteForTeam. If that succeeds, capture request IDs and timestamps and open a Microsoft Graph support case, because Microsoft documents no client-side appId parameter here.