Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Microsoft Teams By James Joyner IV · · 9 min read

Microsoft Teams Error Guide: 'BadRequest / invalidRequest' Malformed Graph API Payload

Fix Microsoft Graph BadRequest and invalidRequest errors on Teams endpoints: bad @odata.type, missing body fields, malformed JSON, and unsupported properties.

  • #microsoft-teams
  • #troubleshooting
  • #errors
  • #graph-api

Overview

BadRequest with inner code invalidRequest is the Microsoft Graph error returned when the request payload is structurally wrong — malformed JSON, an unsupported @odata.type value, a missing required field in the message body, a disallowed property combination, or a Content-Type header mismatch. The endpoint understood the HTTP request but rejected the body before any business logic ran.

The Graph API returns HTTP 400 with a body like:

{
  "error": {
    "code": "BadRequest",
    "message": "Invalid request.",
    "innerError": {
      "code": "invalidRequest",
      "message": "The property 'importanceLevel' does not exist or is not supported for this resource.",
      "date": "2026-06-23T11:45:22",
      "request-id": "c3d4e5f6-3344-5566-7788-99aabbccddee",
      "client-request-id": "e4f5a6b7-5566-7788-99aa-bbccddeeff00"
    }
  }
}

This error surfaces on POST/PATCH calls to Teams endpoints: sending a chat message (POST /v1.0/chats/{id}/messages), creating a channel (POST /v1.0/teams/{id}/channels), updating a tab (PATCH /v1.0/teams/{id}/channels/{id}/tabs/{id}), and similar write operations. GET requests can also return it when query parameters are invalid (malformed $filter or unsupported $expand).

Symptoms

  • POST or PATCH to a Teams / Graph endpoint returns HTTP 400 immediately (no retry loop helps).
  • error.code is BadRequest; error.innerError.code is invalidRequest or UnknownError.
  • The innerError.message names a specific property, type string, or field.
  • The request succeeds in Graph Explorer with a hand-typed payload but fails from the application.
  • Serialized SDK objects include extra properties (nulls, type discriminators) that the API rejects.
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/chats/19:abc123def456@thread.v2/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "body": {
      "contentType": "html",
      "content": "<b>Hello</b>"
    },
    "importanceLevel": "high"
  }' | jq .
{
  "error": {
    "code": "BadRequest",
    "message": "Invalid request.",
    "innerError": {
      "code": "invalidRequest",
      "message": "The property 'importanceLevel' does not exist or is not supported for this resource.",
      "date": "2026-06-23T11:47:08",
      "request-id": "d4e5f6a7-4455-6677-8899-aabbccddeeff"
    }
  }
}

Common Root Causes

1. Wrong or missing @odata.type discriminator

Polymorphic Graph resources (chat message attachments, tab configurations, channel members) require the @odata.type annotation so the deserializer picks the right subtype. An incorrect string or a missing annotation on a required polymorphic field triggers 400.

curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID/members" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "roles": ["owner"],
    "user@odata.bind": "https://graph.microsoft.com/v1.0/users/c3d4e5f6-1234-5678-abcd-ef0123456789"
  }' | jq .error.innerError
{
  "code": "invalidRequest",
  "message": "The odata type '#microsoft.graph.aadUserConversationMember' must be specified for this operation.",
  "date": "2026-06-23T11:50:14",
  "request-id": "e5f6a7b8-5566-7788-99aa-bbccddeeff11"
}

The correct payload must include "@odata.type": "#microsoft.graph.aadUserConversationMember".

2. Required body field missing from the request

Some endpoints require fields that are not obvious from the resource schema. Creating a Teams channel requires displayName; sending a chat message requires the nested body object with contentType and content. Omitting them returns 400.

curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Engineering alerts"
  }' | jq .error.innerError.message
"Required property 'displayName' is missing from the request body."

3. Unsupported or read-only property included in the payload

Properties like id, createdDateTime, webUrl, and etag are server-assigned and must not appear in POST or PATCH bodies. Some SDK serializers include all object properties by default, causing 400 on fields the API marks read-only.

curl -s -X PATCH \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "19:abc123def456@thread.skype",
    "displayName": "Engineering Alerts",
    "createdDateTime": "2026-01-15T09:00:00Z"
  }' | jq .error.innerError.message
"The property 'createdDateTime' is a read-only property and cannot be set."

4. Malformed JSON or incorrect Content-Type header

A missing closing brace, a trailing comma, single-quoted strings, or sending Content-Type: text/plain instead of application/json causes the parser to reject the body before field validation runs.

curl -s -X POST \
  "https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: text/plain" \
  -d '{"body":{"contentType":"text","content":"Hello"}}' | jq .error.code
"BadRequest"

Always set Content-Type: application/json for write operations. Validate JSON with jq empty before sending.

5. Invalid $filter or $select query parameter

Filtering on a non-indexed, non-filterable property, using unsupported OData operators, or requesting a property that does not exist in $select returns 400 on GET calls.

curl -s -G \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
  --data-urlencode "\$filter=createdDateTime ge 2026-01-01T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .error.innerError
{
  "code": "invalidRequest",
  "message": "The query specified in the URI is not valid. The property 'createdDateTime' cannot be used in a $filter expression.",
  "date": "2026-06-23T12:03:41",
  "request-id": "f6a7b8c9-6677-8899-aabb-ccddeeff0022"
}

6. Payload targets an unsupported combination of fields for the channel/chat type

Private channels, shared channels, and standard channels accept different subsets of properties. Sending a membershipType: "private" channel create request with a missing members array, or sending a messagePolicyViolation attachment type to a standard channel, triggers invalidRequest.

curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Incident-Private",
    "membershipType": "private"
  }' | jq .error.innerError.message
"When creating a private channel, at least one owner must be specified in the members field."

Diagnostic Workflow

Step 1: Capture the full error body and note innerError.message

curl -s -X POST \
  "https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD" | jq .error

The innerError.message string names the specific field or constraint that failed. Record the request-id for Microsoft support escalation.

Step 2: Validate JSON syntax before sending

echo "$PAYLOAD" | jq empty && echo "JSON valid" || echo "JSON INVALID"

A non-zero exit code from jq empty means the payload itself is malformed. Fix the syntax first.

Step 3: Compare payload against the Graph API reference schema

Use Graph Explorer (https://developer.microsoft.com/en-us/graph/graph-explorer) to send the same call interactively. Graph Explorer surfaces schema validation errors inline and shows required vs. optional fields per endpoint.

# Retrieve the current resource to see what the API considers valid fields
curl -s -X GET \
  "https://graph.microsoft.com/v1.0/chats/$CHAT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'keys'

Compare returned keys with your POST/PATCH payload to spot read-only or non-writable fields.

Step 4: Verify @odata.type for polymorphic resources

# Correct member add payload
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID/members" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "@odata.type": "#microsoft.graph.aadUserConversationMember",
    "roles": ["member"],
    "user@odata.bind": "https://graph.microsoft.com/v1.0/users/$USER_ID"
  }' | jq '{id, displayName}'

If the response includes an id field, the @odata.type was correct.

Step 5: Test with a minimal required-only payload

Strip the payload to the minimum required fields documented for the endpoint, confirm it succeeds, then add optional fields back one at a time to isolate the offending property.

# Minimal valid chat message
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":{"contentType":"text","content":"test"}}' | jq '{id, createdDateTime}'

Example Root Cause Analysis

An automation script posts structured HTML alerts to a Teams channel using POST /v1.0/teams/{teamId}/channels/{channelId}/messages. The script serializes a Python dataclass with dataclasses.asdict(), which includes id=None, created_date_time=None, and web_url=None as explicit null fields in the JSON output.

The Graph endpoint rejects the payload:

{
  "code": "invalidRequest",
  "message": "The property 'webUrl' is a read-only property and cannot be set."
}

Inspecting the raw payload:

echo "$PAYLOAD" | jq '{id, webUrl, createdDateTime}'
{
  "id": null,
  "webUrl": null,
  "createdDateTime": null
}

Even null values for read-only fields trigger 400 — the field must be absent entirely, not null. The fix is to strip null fields before serialization:

echo "$PAYLOAD" | jq 'del(..|nulls)' > /tmp/clean_payload.json
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d @/tmp/clean_payload.json | jq '{id, createdDateTime}'
{
  "id": "1719144400000",
  "createdDateTime": "2026-06-23T13:00:00.000Z"
}

The message posts successfully after removing all null-valued properties.

Prevention Best Practices

  • Use the official Microsoft Graph SDK for your language rather than hand-rolling JSON payloads; the SDK handles @odata.type annotations and omits read-only fields automatically on write operations.
  • Add a jq empty or equivalent JSON validation step in your CI pipeline before any Graph API call — malformed JSON is always a code bug, not a runtime condition.
  • Pin to the Graph API v1.0 surface for production integrations; beta endpoint schemas change without notice and are a common source of suddenly invalid field names.
  • When serializing objects to JSON for Graph writes, strip nulls and undefined properties (jq 'del(..|nulls)' or SDK SerializerOption.SkipNullValues) — the Graph API does not treat null and absent as equivalent.
  • Maintain a canary test that POSTs a minimal valid payload to each Teams endpoint your integration touches and asserts HTTP 201/200; this catches schema changes before they affect production traffic.
  • Pair Graph 400 errors with the incident assistant to cross-reference innerError.message text against known field-constraint patterns quickly.

Quick Command Reference

# Validate JSON payload before sending
echo "$PAYLOAD" | jq empty && echo "valid" || echo "INVALID"

# Strip null fields from payload
echo "$PAYLOAD" | jq 'del(..|nulls)' > /tmp/clean.json

# Send a chat message (minimal valid payload)
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":{"contentType":"text","content":"test"}}' | jq .

# Add a member to a channel (with required @odata.type)
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels/$CHANNEL_ID/members" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "@odata.type": "#microsoft.graph.aadUserConversationMember",
    "roles": ["member"],
    "user@odata.bind": "https://graph.microsoft.com/v1.0/users/$USER_ID"
  }' | jq .

# Create a private channel (members required)
curl -s -X POST \
  "https://graph.microsoft.com/v1.0/teams/$TEAM_ID/channels" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "@odata.type": "#microsoft.graph.channel",
    "displayName": "Incident-Private",
    "membershipType": "private",
    "members": [
      {
        "@odata.type": "#microsoft.graph.aadUserConversationMember",
        "roles": ["owner"],
        "user@odata.bind": "https://graph.microsoft.com/v1.0/users/$OWNER_ID"
      }
    ]
  }' | jq .

# Inspect an existing resource to identify valid field names
curl -s "https://graph.microsoft.com/v1.0/chats/$CHAT_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'keys'

Conclusion

BadRequest / invalidRequest from Microsoft Graph on Teams endpoints always points to a payload problem, not a permission or authentication issue. The six root causes in order of frequency:

  1. A missing or incorrect @odata.type discriminator on a polymorphic resource (member, attachment, tab configuration).
  2. A required field (displayName, body.contentType, owner members array) is absent from the request body.
  3. A read-only server-assigned property (id, createdDateTime, webUrl) was included in the write payload, even as null.
  4. Malformed JSON or wrong Content-Type header causes the parser to reject the body before field validation.
  5. An invalid $filter or $select query parameter referencing a non-filterable or non-existent property.
  6. An unsupported field combination for the specific channel/chat type (private channel without owner, restricted attachment type).

The fastest fix path is always: validate JSON syntax first, then read the innerError.message to identify the exact field, then compare against the Microsoft Teams guides and strip or correct that field from the payload.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.