Guest API POST /api/v2/conversations/messaging/external/threads returns 400 with 'Invalid sender'

Quick question about implementing the Guest API for a custom web chat interface. We are bypassing the standard Messenger widget to build a native iOS and Android experience. The goal is to handle the full lifecycle: create thread, send message, receive webhook.

We are successfully creating the thread using POST /api/v2/conversations/messaging/external/threads. We get back the externalId and threadId. The payload for this step is standard:

{
 "externalId": "user-123-unique",
 "name": "Test User",
 "email": "[email protected]"
}

This returns a 201 Created. So far so good. The issue arises when we attempt to send the initial message using POST /api/v2/conversations/messaging/external/threads/{externalId}/messages. We are using the exact same externalId from the thread creation response. The body contains a simple text message:

{
 "text": "Hello support"
}

We are getting a 400 Bad Request with the following error:

{
 "code": "bad_request",
 "message": "Invalid sender. The external ID does not match the thread owner.",
 "details": []
}

I have double-checked the headers. The Authorization header contains a valid Bearer token with the messaging:external:write scope. The Content-Type is application/json. I am not seeing any discrepancy between the externalId used in the URL and the one created in the previous step.

Is there a specific header or context required in the message creation request to bind it to the thread owner? Or is the Guest API stateful in a way that requires a session token from the thread creation response that I am missing? I am working from Berlin, so timezone shouldn’t be an issue, but I want to rule out any clock skew if that matters for the webhook validation later.

Any insight into the correct payload structure or authentication flow for the message endpoint would be appreciated.

I’d suggest checking out at how the PureCloudPlatformClientV2 PHP SDK handles the startChat payload versus subsequent attribute updates. The suggestion above about schema compliance is valid, but often…

The senderId in your message payload must match the externalId from the thread creation response exactly. Double-check your JSON structure to ensure sender.id is a string.

{
 "sender": {
 "id": "your-external-id-here",
 "name": "Guest"
 }
}

If I recall correctly, the sender.id validation is strict. The suggestion above correctly identifies the string type requirement, but the value must also match the externalId returned by the thread creation endpoint. Mismatches here trigger the 400 error immediately.

  1. Verify the externalId from the POST /api/v2/conversations/messaging/external/threads response.
  2. Assign this exact string to the sender.id field in your message payload.
{
 "sender": {
 "id": "{{external_id_from_thread_creation}}"
 },
 "text": "Hello"
}

In my Terraform modules, I enforce this linkage via output references to prevent drift. Ensure your client code does not generate a new UUID for the sender. The backend expects identity continuity from the initial handshake.

I’d recommend looking at at your JSON payload structure. The sender object is nested, and the API is strict about the id field matching the externalId from the thread creation response.

Ensure you are passing the string exactly as returned. A common pitfall is using the internal threadId or a UUID format instead of the custom externalId you supplied during initialization.

{
 "sender": {
 "id": "your-external-id-here",
 "name": "User Name"
 },
 "text": "Hello"
}

Check your OAuth token scopes and sender ID mapping. The previous answers are correct about the sender.id matching the externalId, but I often see this fail due to scope resolution in client-side apps. Ensure your token includes messaging:send and messaging:guest:write. Also, verify the sender.id is a plain string, not a UUID or object.

  1. Create thread: POST /api/v2/conversations/messaging/external/threads
  2. Extract externalId from response.
  3. Send message: POST /api/v2/conversations/messaging/{conversationId}/messages
  • Payload:
{
"sender": {
"id": "your-external-id",
"name": "Guest User"
},
"text": "Hello"
}
  • Header: Content-Type: application/json

If the externalId contains special characters, ensure it is URL-encoded in the request body. This strict validation prevents spoofing. Double-check your client app SDK configuration for implicit grant with PKCE to ensure tokens are valid and have the right scopes.