Share via

Single-Tenant Bot receives 403 (empty body) when calling Bot Connector API sendToConversation — all settings verified correct

Shingo Takada 20 Reputation points
2026-05-11T16:22:00.2133333+00:00
## Description

I have a Single-Tenant Azure Bot that receives messages correctly but cannot send replies via the Bot Connector API. The API returns 403 with an empty response body.

### Environment

- **Bot Resource**: ai-agent-hub-teams-bot (Single-Tenant)
- **Microsoft App ID**: 330b21ae-f364-4abf-8e5a-f44a9fcb2c24
- **Tenant ID**: 88417d26-8aa1-45b8-9957-4ecd806539fe
- **Region**: ap-northeast-1 (messaging endpoint on AWS Lambda)
- **Channels**: Web Chat (Healthy), Teams (Healthy), Direct Line (Healthy)
- **Pricing Tier**: Free (F0)
- **SDK**: botframework-connector v4.23.0 (Node.js)

### What works

- Token acquisition: Successfully obtains token from `login.microsoftonline.com/{tenantId}/oauth2/v2.0/token` with scope `https://api.botframework.com/.default`
- Token claims are correct: `aud: https://api.botframework.com`, `appid: 330b21ae-...`, `tid: 88417d26-...`
- Message reception: Bot Service → Lambda webhook works perfectly (messages arrive, are processed)
- Channels show "Healthy" status in Azure Portal

### What fails

Calling `POST https://webchat.botframework.com/v3/conversations/{conversationId}/activities` returns **403 with empty body**.

### Verification tests performed

I systematically verified each component by changing settings and observing error changes:

| Test | Result | What it proves |
|---|---|---|
| Changed scope to `api://botid-{appId}` | 403 → **401** | Original scope is correct (token is accepted with default scope) |
| Removed `from` field from Activity | 403 → **400** "Activity.From field is required" | `from: { id: appId }` passes validation (format is correct) |
| All correct settings combined | **403** (empty body) | Authentication passes, request format is valid, but authorization fails |

### What this proves

1. **Token is correct** — changing the audience causes 401, so the original token is accepted
2. **URL is correct** — the API responds (not 404 or connection error)
3. **Request body is correct** — removing required fields causes 400, so the body passes validation
4. **The 403 is purely an authorization issue** — everything is correct but the bot is denied permission to send to the conversation

### Configuration verified

- App Registration: signInAudience = `AzureADMyOrg`, accessTokenAcceptedVersion = 2
- identifierUris: `api://botid-330b21ae-f364-4abf-8e5a-f44a9fcb2c24` (set)
- Bot Service messaging endpoint: `https://aordy0pta9.execute-api.ap-northeast-1.amazonaws.com/dev/teams/webhook`
- All channels show Healthy

### x-ms-request-id values (for Azure-side investigation)

- `71752c4e2ec602ed25f7a098c29ef7e1` (403 with correct settings)
- `35bc3a889ee194283aff07caae6cf6d7` (403 with correct settings)
- `77a33f11a3423ac3fd13e57819025baa` (403 with correct settings)

### Question

What authorization check is failing? The bot is authenticated correctly (proven by 401 test), the request format is valid (proven by 400 test), but the bot is denied permission to send messages to conversations that it is a member of. Is there an additional configuration required for Single-Tenant bots to use the Bot Connector API for sending messages?

### Code (simplified)

```typescript
import { MicrosoftAppCredentials, ConnectorClient } from "botframework-connector";

const credentials = new MicrosoftAppCredentials(appId, appPassword, tenantId);
MicrosoftAppCredentials.trustServiceUrl(serviceUrl);
const client = new ConnectorClient(credentials, { baseUri: serviceUrl });

await client.conversations.sendToConversation(conversationId, {
  type: "message",
  text: "Hello",
  from: { id: appId, name: "Bot" },
});
// Result: 403 (empty body)

Azure AI Bot Service
Azure AI Bot Service

An Azure service that provides an integrated environment for bot development.

0 comments No comments

Answer accepted by question author

Amira Bedhiafi 41,641 Reputation points MVP Volunteer Moderator
2026-05-11T18:20:12.69+00:00

Hello Shingo !

Thank you for posting on MS Learn Q&A.

I think the Bot connector conversation auth check is failing.

Your token can be valid and the JSON can be syntactically valid but Bot connector still checks whether the bot identity is allowed to send as the bot member of that exact conversation at that exact serviceUrl. The replies must use the serviceUrl from the incoming activity and their example sends the reply with from.id equal to the bot account from the incoming activity recipient not manually constructed from the App ID.

In your sample,I have a doubt with this line:

from: { id: appId, name: "Bot" }

For Teams, the bot channel account is commonly not just the raw App ID and it can be like:

28:<appId>

For Web Chat also it may also differ from what you expect. A missing from gives 400 because the schema is invalid but a present but wrong from.id can pass schema validation and then fail the auth check with 403.

You can use the incoming activity conversation reference instead of rebuilding fields manually:

import { TurnContext } from "botbuilder";
import { MicrosoftAppCredentials, ConnectorClient } from "botframework-connector";
const reference = TurnContext.getConversationReference(incomingActivity);
const credentials = new MicrosoftAppCredentials(appId, appPassword, tenantId);
MicrosoftAppCredentials.trustServiceUrl(reference.serviceUrl);
const client = new ConnectorClient(credentials, {
  baseUri: reference.serviceUrl,
});
await client.conversations.sendToConversation(reference.conversation.id, {
  type: "message",
  text: "Hello",
  // use the bot account from the incoming activity
  from: reference.bot,
  // usually include these too
  recipient: reference.user,
  conversation: reference.conversation,
  channelId: reference.channelId,
  serviceUrl: reference.serviceUrl,
});

For a direct reply to the message that triggered the turn also test:

await client.conversations.replyToActivity(
  incomingActivity.conversation.id,
  incomingActivity.id,
  {
    type: "message",
    text: "Hello",
    from: incomingActivity.recipient,
    recipient: incomingActivity.from,
    conversation: incomingActivity.conversation,
    channelId: incomingActivity.channelId,
    serviceUrl: incomingActivity.serviceUrl,
  }
);

Was this answer helpful?

1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. SRILAKSHMI C 18,385 Reputation points Microsoft External Staff Moderator
    2026-05-18T17:22:46.0533333+00:00

    Hello @Shingo Takada

    Thank you for the detailed investigation and validation steps, strongly suggests this is not an authentication failure, but rather a connector-level authorization mismatch during outbound activity validation.

    From the behavior you described:

    • 401 occurs only when the token audience/scope is intentionally incorrect
    • 400 occurs when required activity fields are removed
    • 403 occurs only when everything appears valid

    This confirms:

    • token acquisition is working,
    • the JWT is accepted,
    • the request body passes schema validation,
    • but the Bot Connector service is rejecting the outbound conversation authorization step.

    The most likely cause here is a mismatch between:

    • the bot identity,
    • the conversation/channel binding,
    • and the serviceUrl being used for the outbound request.

    A few important things stand out from your example.

    For Single-Tenant bots, the Bot Connector Service performs stricter validation to ensure:

    • the bot identity in the JWT matches the bot identity known to the conversation,
    • the conversation belongs to the same channel/serviceUrl scope,
    • and the bot is authorized for that specific conversation instance.

    One important detail: You are sending requests to:

    https://webchat.botframework.com/v3/conversations/{conversationId}/activities
    

    However, conversation IDs and service URLs are channel-specific.

    For example:

    • Teams conversations must use the Teams serviceUrl
    • Web Chat conversations must use the Web Chat connector
    • Direct Line conversations require Direct Line endpoints/tokens

    A Teams conversation cannot reliably be reused against the Web Chat connector endpoint.

    This can produce the exact silent 403 behavior you are seeing.

    Another likely contributor is the from.id value.

    In your snippet you are explicitly setting:

    from: { id: appId, name: "Bot" }
    

    However, the connector service validates the bot identity against the channel-specific bot ID not always the raw Azure AD App ID.

    For Teams/Web Chat, the bot identity is often something like:

    28:<bot-guid>
    

    rather than:

    330b21ae-f364-...
    

    If those identities do not match exactly, the connector authorization layer may reject the send operation with a 403.

    Recommended changes:

    1. Do not hardcode the from field

    Either omit the from field entirely and allow the SDK/ConnectorClient to populate it automatically, or use the inbound activity recipient ID:

    from: {
      id: turnContext.activity.recipient.id
    }
    

    instead of the Azure AD App ID.

    1. Use the exact inbound serviceUrl

    Do not hardcode:

    https://webchat.botframework.com
    

    Instead use:

    const serviceUrl = turnContext.activity.serviceUrl;
    MicrosoftAppCredentials.trustServiceUrl(serviceUrl);
    const client = new ConnectorClient(credentials, {
      baseUri: serviceUrl
    });
    

    The outbound request must use the exact same serviceUrl that arrived on the inbound activity.

    1. Ensure conversationId + serviceUrl originate from the same activity

    The following values must belong together:

    • conversation.id
    • channelId
    • serviceUrl

    Mixing them across channels causes connector authorization failure.

    1. Channel-specific note

    For Teams specifically outbound sends/proactive messaging require the bot to be installed in that exact tenant/chat/team scope, and the connector validates tenant/conversation membership before allowing sends.

    Even if inbound messages succeed, outbound sends can still fail if the connector does not recognize the bot as authorized for that specific conversation context.

    1. Direct Line / Web Chat clarification

    Web Chat and Direct Line use different authentication expectations than Teams.

    If using:

    https://directline.botframework.com
    

    you generally need Direct Line tokens/secrets rather than Connector API AAD tokens.

    Using ConnectorClient and https://api.botframework.com/.default against the wrong channel endpoint can also result in silent 403 responses.

    Please refer this

    Azure Bot Service authentication troubleshooting https://docs.microsoft.com/azure/bot-service/bot-service-troubleshoot-authentication-problems?view=azure-bot-service-4.0

    Teams bot status codes (403 = InvalidBotApiHost / NotEnoughPermissions) https://learn.microsoft.com/microsoftteams/platform/bots/build-conversational-capability?wt.mc_id=knowledgesearch_inproduct_azure-cxp-community-insider#status-codes-from-bot-conversational-apis

    Direct Line REST API reference (HTTP status codes) https://docs.microsoft.com/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-api-reference?view=azure-bot-service-4.0#http-status-codes

    General HTTP-error guide for Bot Service https://docs.microsoft.com/azure/bot-service/bot-service-troubleshoot-general-problems?view=azure-bot-service-4.0

    I Hope this helps. Do let me know if you have any further queries.


    If this answers your query, please do click Accept Answer and Yes for was this answer helpful.

    Thank you!

    Was this answer helpful?

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.