signalapp/Signal-Server · error · NotAuthorizedException

A group send endorsement token or unidentified access key…

Error message

A group send endorsement token or unidentified access key is required for non-story messages

What it means

Non-story multi-recipient messages require some form of authorization: either a group send endorsement token header or a combined unidentified-sender access key. Requests with neither receive a 401 (NotAuthorizedException). The server refuses unauthenticated non-story fan-out.

Solutions

  1. Obtain a group send endorsement token from the endorsements endpoint and attach it to the request.
  2. Alternatively compute and attach the combined unidentified-sender access keys header for the recipient set.
  3. Ensure the message is genuinely a story if you intend to send without authentication.

Example fix

// before
// no auth headers set for non-story group send -> 401
// after
GroupSendToken token = endorsementClient.fetchEndorsement(recipients);
builder.header("X-Group-Send-Token", token.serialize());
sendMultiRecipient(builder.build());
Defensive patterns

Strategy: try-catch

Validate before calling

if (!isStory && headers["X-Group-Send-Token"] == null && headers["X-Unidentified-Access-Keys"] == null) { await fetchEndorsements(recipients); }

Type guard

function hasAuth(headers) { return headers["X-Group-Send-Token"] != null || headers["X-Unidentified-Access-Keys"] != null; }

Try / catch

try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 401 && /endorsement token or unidentified access key/.test(e.body)) { await fetchEndorsements(); retrySend(); } }

Prevention

When it happens

Trigger: POST to the multi-recipient endpoint with a non-story message where both the group send token header and the combined unidentified access key header are absent/null.

Common situations: Older client versions predating group send endorsements that never set the access-key header; header stripped by a proxy; client code path forgot to fetch endorsements before a group send.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/a6e1d819de77f655. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java:539

      sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);
    }
  }

  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);

View on GitHub (pinned to 100ab61c82)