{"record":{"id":"68814edf9ef16024","repo":"signalapp/Signal-Server","slug":"multi-recipient-messages-must-be-addressed-to-aci","errorCode":null,"errorMessage":"Multi-recipient messages must be addressed to ACI service IDs","messagePattern":"Multi-recipient messages must be addressed to ACI service IDs","errorType":"http","errorClass":"WebApplicationException","httpStatus":401,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":716,"sourceCode":"  private void checkGroupSendToken(final Collection<ServiceId> recipients, final GroupSendFullToken groupSendFullToken) {\n    try {\n      groupSendFullToken.verify(recipients,\n          clock.instant(),\n          GroupSendDerivedKeyPair.forExpiration(groupSendFullToken.getExpiration(), serverSecretParams));\n    } catch (final VerificationFailedException e) {\n      throw new NotAuthorizedException(e);\n    }\n  }\n\n  private void checkAccessKeys(\n      final @NotNull CombinedUnidentifiedSenderAccessKeys accessKeys,\n      final SealedSenderMultiRecipientMessage multiRecipientMessage,\n      final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients) {\n\n    if (multiRecipientMessage.getRecipients().keySet().stream()\n        .anyMatch(serviceId -> serviceId instanceof ServiceId.Pni)) {\n\n      throw new WebApplicationException(\"Multi-recipient messages must be addressed to ACI service IDs\",\n          Status.UNAUTHORIZED);\n    }\n\n    try {\n      if (!UnidentifiedAccessUtil.checkUnidentifiedAccess(resolvedRecipients.values(), accessKeys.getAccessKeys())) {\n        throw new WebApplicationException(Status.UNAUTHORIZED);\n      }\n    } catch (final IllegalArgumentException ignored) {\n      throw new WebApplicationException(Status.UNAUTHORIZED);\n    }\n  }\n\n  @POST\n  @Consumes(MediaType.APPLICATION_JSON)\n  @Path(\"/report/{source}/{messageGuid}\")\n  public Response reportSpamMessage(\n      @Auth AuthenticatedDevice auth,\n      @PathParam(\"source\") String source,","sourceCodeStart":698,"sourceCodeEnd":734,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L698-L734","documentation":"Multi-recipient messages may only be addressed to ACI (account identity) service IDs; PNIs (phone-number identities) are rejected. checkAccessKeys returns HTTP 401 with this message when any recipient in the payload uses a PNI.","triggerScenarios":"Including a ServiceId.Pni in the recipient set of a POST to the multi-recipient message endpoint.","commonSituations":"Client resolving contacts by phone number and using the PNI instead of the ACI when building a sealed-sender story send.","solutions":["Address every recipient by their ACI (UUID), not their PNI","Look up the ACI from the contact/profile instead of deriving the service ID from the phone number","Validate the recipient list client-side and strip/filter PNI entries before sending"],"exampleFix":"// before\nif (serviceId instanceof ServiceId.Pni) { /* add recipient */ }\n// after\nif (serviceId instanceof ServiceId.Aci) { /* add recipient */ }","handlingStrategy":"validation","validationCode":"function allAci(serviceIds) {\n  return serviceIds.every(id => id.startsWith('00000000-0000-0000-0000-0000000000') === false && isAciUuid(id));\n}\n// or: verify ids came from ACI (identity key) lookups, not PNI (phone-number) lookups","typeGuard":"function isAci(serviceId) { return serviceId.type === 'ACI'; }","tryCatchPattern":"try { await send(msg); } catch (e) { if (e.status === 401 && /ACI service IDs/.test(e.message)) readdressToAciAndRetry(msg); else throw e; }","preventionTips":["Always address sealed-sender multi-recipient messages by ACI","Keep ACI and PNI lookups in clearly separate code paths","Validate the recipient list before serializing the payload"],"tags":["http-401","signal-server","multi-recipient","service-id"],"backgroundTag":"invalid-argument-value","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"}