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.codeisBadRequest;error.innerError.codeisinvalidRequestorUnknownError.- The
innerError.messagenames 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.typeannotations and omits read-only fields automatically on write operations. - Add a
jq emptyor 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 SDKSerializerOption.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.messagetext 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:
- A missing or incorrect
@odata.typediscriminator on a polymorphic resource (member, attachment, tab configuration). - A required field (
displayName,body.contentType, ownermembersarray) is absent from the request body. - A read-only server-assigned property (
id,createdDateTime,webUrl) was included in the write payload, even as null. - Malformed JSON or wrong
Content-Typeheader causes the parser to reject the body before field validation. - An invalid
$filteror$selectquery parameter referencing a non-filterable or non-existent property. - 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.
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.