signalapp/Signal-Server · error · BadRequestException
Group send endorsement tokens should not be combined with…
Error message
Group send endorsement tokens should not be combined with other authentication
What it means
The sendMessage endpoint rejects requests that supply a group send endorsement token header together with another form of sender authentication (an explicit source account or unidentified access key). Group send endorsement tokens are a standalone authentication mechanism, so combining them is invalid and yields a 400.
Solutions
- Remove either the group send endorsement token header or the source/accessKey authentication from the request.
- If using endorsements, send the token alone with no account credential headers.
- Update client code to choose one authentication mode per request explicitly.
Example fix
// before
request.header("X-Group-Send-Token", token).header("Authorization", accountAuth); // conflicting
// after
if (useGroupSendToken) { request.header("X-Group-Send-Token", token); } else { request.header("Authorization", accountAuth); } Defensive patterns
Strategy: validation
Validate before calling
const hasToken = headers["X-Group-Send-Token"] != null;
const hasOtherAuth = headers["Authorization"] != null || headers["X-Unidentified-Access-Key"] != null;
if (hasToken && hasOtherAuth) throw new Error("choose one auth mode"); Try / catch
try { await send(messages); } catch (e) { if (e.status === 400 && /other authentication/.test(e.body)) { stripRedundantAuthHeadersAndRetry(); } } Prevention
- Pick one authentication mode per send request explicitly
- Audit middleware/proxies that auto-inject auth headers
- During auth migration, remove legacy headers when enabling endorsements
When it happens
Trigger: POST /v1/messages with the group send endorsement token header set while also providing an authorization (account) identity or unidentified-sender access key header.
Common situations: Clients migrating from access-key auth to group send endorsements leaving both headers in place; middleware/proxies injecting default auth headers; copy-pasted header configuration.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Only one of group send endorsement token and unidentified…
- Group send endorsement tokens should not be sent for story…
- Group send token not allowed when sending stories
- A group send endorsement token or unidentified access key…
- Operation requires unauthenticated access
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/f1ce29b4b3172ca6.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java:221
@HeaderParam(HeaderUtils.GROUP_SEND_TOKEN)
@Nullable final GroupSendTokenHeader groupSendToken,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
@Parameter(description="The recipient’s account or phone-number identifier")
@PathParam("destination") final ServiceIdentifier destinationIdentifier,
@Parameter(description="If true, the message is a story; access tokens are not checked and sending to nonexistent recipients is permitted")
@QueryParam("story") final boolean isStory,
@Parameter(description="The encrypted message payloads for each recipient device")
@NotNull @Valid final IncomingMessageList messages,
@Context final ContainerRequestContext context) throws RateLimitExceededException {
if (groupSendToken != null) {
if (source.isPresent() || accessKey.isPresent()) {
throw new BadRequestException("Group send endorsement tokens should not be combined with other authentication");
} else if (isStory) {
throw new BadRequestException("Group send endorsement tokens should not be sent for story messages");
}
}
final Sample sample = Timer.start();
final boolean needsSync;
try {
if (isStory) {
needsSync = false;
sendStoryMessage(destinationIdentifier, messages, context);
} else if (source.isPresent()) {
final AuthenticatedDevice authenticatedDevice = source.get();
final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
.orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));
if (account.isIdentifiedBy(destinationIdentifier)) {View on GitHub (pinned to 100ab61c82)