signalapp/Signal-Server · error · BadRequestException

Group send token not allowed when sending stories

Error message

Group send token not allowed when sending stories

What it means

Stories on the multi-recipient path require no authentication, so providing a group send token header with a story send is rejected with a 400. (For historical reasons a combined unidentified-access key is tolerated but ignored.)

Solutions

  1. Do not attach the group send token header when the message is a story.
  2. Route story sends through the story-specific path without endorsement headers.
  3. Audit client request-building code to conditionally set the token based on message type.

Example fix

// before
builder.header("X-Group-Send-Token", token);
sendMultiRecipient(payload, /* isStory */ true);
// after
if (!isStory) { builder.header("X-Group-Send-Token", token); }
sendMultiRecipient(payload, isStory);
Defensive patterns

Strategy: validation

Validate before calling

if (isStory && headers["X-Group-Send-Token"]) { delete headers["X-Group-Send-Token"]; }

Try / catch

try { await sendMultiRecipient(payload, isStory); } catch (e) { if (e.status === 400 && /not allowed when sending stories/.test(e.body)) { resendWithoutToken(); } }

Prevention

When it happens

Trigger: POST to the multi-recipient endpoint with the story flag set and the group send endorsement token header present.

Common situations: Client attaches group send tokens to every multi-recipient send including stories; story support added to a code path that always sets the token header; reused request builders.

Related errors


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

Appendix: source

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

    if (timestamp < 0 || timestamp > MAX_TIMESTAMP) {
      throw new BadRequestException("Illegal timestamp");
    }

    if (multiRecipientMessage.getRecipients().isEmpty()) {
      throw new BadRequestException("Recipient list is empty");
    }

    final Timer.Sample sample = Timer.start();

    try {
      final SendMultiRecipientMessageResponse sendMultiRecipientMessageResponse;

      if (isStory) {
        if (groupSendToken != null) {
          // Stories require no authentication. We fail requests that provide a groupSendToken, but for historical
          // reasons we allow requests to set a combined access key, even though we ignore it
          throw new BadRequestException("Group send token not allowed when sending stories");
        }

        sendMultiRecipientMessageResponse =
            sendMultiRecipientStoryMessage(multiRecipientMessage, timestamp, online, isUrgent, context);
      } else {
        sendMultiRecipientMessageResponse =
            sendMultiRecipientMessage(multiRecipientMessage, timestamp, online, isUrgent, groupSendToken, accessKeys,
                context);
      }

      return Response.ok(sendMultiRecipientMessageResponse).build();
    } finally {
      sample.stop(MULTI_RECIPIENT_MESSAGE_LATENCY_TIMER);
    }
  }

  private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,
      final long timestamp,

View on GitHub (pinned to 100ab61c82)