signalapp/Signal-Server · error · BadRequestException
Only one of group send endorsement token and unidentified…
Error message
Only one of group send endorsement token and unidentified access key may be provided
What it means
The group send endorsement token and the combined unidentified-sender access keys are alternative authentication mechanisms for multi-recipient sends; supplying both is rejected with a 400. Exactly one must be provided for non-story messages.
Solutions
- Send exactly one: prefer the group send endorsement token on newer clients and drop the access-key header.
- Make the client's auth-mode selection exclusive (if/else, not two independent header setters).
- During migration, disable legacy access-key attachment once endorsements are confirmed working.
Example fix
// before
builder.header("X-Group-Send-Token", token);
builder.header("X-Unidentified-Access-Keys", combinedKeys); // both set
// after
if (token != null) { builder.header("X-Group-Send-Token", token); }
else { builder.header("X-Unidentified-Access-Keys", combinedKeys); } Defensive patterns
Strategy: validation
Validate before calling
if (headers["X-Group-Send-Token"] != null && headers["X-Unidentified-Access-Keys"] != null) { delete headers["X-Unidentified-Access-Keys"]; } Type guard
function hasExactlyOneAuth(headers) { const n = [headers["X-Group-Send-Token"], headers["X-Unidentified-Access-Keys"]].filter(h => h != null).length; return n === 1; } Try / catch
try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 400 && /Only one of/.test(e.body)) { resendWithSingleAuthMode(); } } Prevention
- Make endorsement vs access-key selection an if/else, not two independent header setters
- During migration, remove access-key headers once endorsements work
- Assert single-auth-mode in client unit tests
When it happens
Trigger: POST to the multi-recipient endpoint with both the group send endorsement token header and the combined unidentified access keys header set on a non-story message.
Common situations: Client migration from access keys to endorsements leaving both headers during the transition; default header injection plus explicit token; conditional logic that falls through and sets both.
Related errors
- Group send endorsement tokens should not be combined with…
- 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/52248672f2852f99.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java:543
private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,
final long timestamp,
final boolean ephemeral,
final boolean urgent,
@Nullable final GroupSendTokenHeader groupSendTokenHeader,
@Nullable final CombinedUnidentifiedSenderAccessKeys combinedUnidentifiedSenderAccessKeys,
final ContainerRequestContext context) {
// Perform fast, inexpensive checks before attempting to resolve recipients
if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {
throw new BadRequestException("Multi-recipient message contains duplicate recipient");
}
if (groupSendTokenHeader == null && combinedUnidentifiedSenderAccessKeys == null) {
throw new NotAuthorizedException("A group send endorsement token or unidentified access key is required for non-story messages");
}
if (groupSendTokenHeader != null && combinedUnidentifiedSenderAccessKeys != null) {
throw new BadRequestException("Only one of group send endorsement token and unidentified access key may be provided");
}
if (groupSendTokenHeader != null) {
// Group send endorsements are checked before we even attempt to resolve any accounts, since
// the lists of service IDs in the envelope are all that we need to check against
checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendTokenHeader);
} else {
Metrics.counter(LEGACY_COMBINED_UAK_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(context))).increment();
}
// At this point, the caller has at least superficially provided the information needed to send a multi-recipient
// message. Attempt to resolve the destination service identifiers to Signal accounts.
final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients =
MessageUtil.resolveRecipients(accountsManager, multiRecipientMessage);
final List<ServiceIdentifier> unresolvedRecipientServiceIdentifiers =
MessageUtil.getUnresolvedRecipients(multiRecipientMessage, resolvedRecipients);
View on GitHub (pinned to 100ab61c82)