{"record":{"id":"a6e1d819de77f655","repo":"signalapp/Signal-Server","slug":"a-group-send-endorsement-token-or-unidentified-acc","errorCode":null,"errorMessage":"A group send endorsement token or unidentified access key is required for non-story messages","messagePattern":"A group send endorsement token or unidentified access key is required for non-story messages","errorType":"http","errorClass":"NotAuthorizedException","httpStatus":401,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":539,"sourceCode":"      sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);\n    }\n  }\n\n  private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,\n      final long timestamp,\n      final boolean ephemeral,\n      final boolean urgent,\n      @Nullable final GroupSendTokenHeader groupSendTokenHeader,\n      @Nullable final CombinedUnidentifiedSenderAccessKeys combinedUnidentifiedSenderAccessKeys,\n      final ContainerRequestContext context) {\n\n    // Perform fast, inexpensive checks before attempting to resolve recipients\n    if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {\n      throw new BadRequestException(\"Multi-recipient message contains duplicate recipient\");\n    }\n\n    if (groupSendTokenHeader == null && combinedUnidentifiedSenderAccessKeys == null) {\n      throw new NotAuthorizedException(\"A group send endorsement token or unidentified access key is required for non-story messages\");\n    }\n\n    if (groupSendTokenHeader != null && combinedUnidentifiedSenderAccessKeys != null) {\n      throw new BadRequestException(\"Only one of group send endorsement token and unidentified access key may be provided\");\n    }\n\n    if (groupSendTokenHeader != null) {\n      // Group send endorsements are checked before we even attempt to resolve any accounts, since\n      // the lists of service IDs in the envelope are all that we need to check against\n      checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendTokenHeader);\n    } else {\n      Metrics.counter(LEGACY_COMBINED_UAK_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(context))).increment();\n    }\n\n    // At this point, the caller has at least superficially provided the information needed to send a multi-recipient\n    // message. Attempt to resolve the destination service identifiers to Signal accounts.\n    final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients =\n        MessageUtil.resolveRecipients(accountsManager, multiRecipientMessage);","sourceCodeStart":521,"sourceCodeEnd":557,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L521-L557","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Obtain a group send endorsement token from the endorsements endpoint and attach it to the request.","Alternatively compute and attach the combined unidentified-sender access keys header for the recipient set.","Ensure the message is genuinely a story if you intend to send without authentication."],"exampleFix":"// before\n// no auth headers set for non-story group send -> 401\n// after\nGroupSendToken token = endorsementClient.fetchEndorsement(recipients);\nbuilder.header(\"X-Group-Send-Token\", token.serialize());\nsendMultiRecipient(builder.build());","handlingStrategy":"try-catch","validationCode":"if (!isStory && headers[\"X-Group-Send-Token\"] == null && headers[\"X-Unidentified-Access-Keys\"] == null) { await fetchEndorsements(recipients); }","typeGuard":"function hasAuth(headers) { return headers[\"X-Group-Send-Token\"] != null || headers[\"X-Unidentified-Access-Keys\"] != null; }","tryCatchPattern":"try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 401 && /endorsement token or unidentified access key/.test(e.body)) { await fetchEndorsements(); retrySend(); } }","preventionTips":["Fetch group send endorsements before every non-story multi-recipient send","Confirm proxies do not strip authentication headers","Upgrade old clients that predate endorsement/access-key headers"],"tags":["http-401","authentication","group-send-endorsements"],"backgroundTag":"authentication-required","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}