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

  1. Send exactly one: prefer the group send endorsement token on newer clients and drop the access-key header.
  2. Make the client's auth-mode selection exclusive (if/else, not two independent header setters).
  3. 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

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


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)