signalapp/Signal-Server · error · BadRequestException

login purchases are not enabled

Error message

login purchases are not enabled

What it means

LoginPurchaseController.createLoginReceiptCredential is gated by a server-side dynamic configuration flag, loginPurchaseConfiguration.enabled(). When the flag is off, the endpoint throws BadRequestException('login purchases are not enabled') before processing anything else. It is an intentional feature-gate, not a client bug.

Solutions

  1. Enable login purchases in the server's dynamic configuration (set loginPurchaseConfiguration.enabled to true and reload)
  2. Wait for the operator to enable the feature flag if you don't control the server
  3. Confirm you are pointing at the environment where the feature is enabled
  4. Handle the 400 client-side by disabling login-purchase UI when the server rejects it

Example fix

# server dynamic configuration
// before
"loginPurchase": { "enabled": false }
// after
"loginPurchase": { "enabled": true }  # then reload dynamic configuration
Defensive patterns

Strategy: try-catch

Try / catch

try {
  client.createLoginReceiptCredential(request);
} catch (WebApplicationException e) {
  if (e.getResponse().getStatus() == 400 && String.valueOf(e.getResponse().getEntity()).contains("not enabled")) {
    // hide login-purchase UI; feature disabled server-side
  } else throw e;
}

Prevention

When it happens

Trigger: POST to the login purchase receipt-credential endpoint while the server's DynamicConfiguration loginPurchaseConfiguration.enabled() is false.

Common situations: Staging/self-hosted servers without the feature flag enabled in dynamic configuration; testing before an ops rollout; clients built against a server where the feature was temporarily disabled.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java:108

      """)
  @ApiResponse(responseCode = "402", description = "The purchase did not complete successfully. The body may include ChargeFailure details.",
      content = @Content(schema = @Schema(
          nullable = true,
          implementation = SubscriptionExceptionMapper.ChargeFailureResponse.class)))
  @ApiResponse(responseCode = "403", description = "The request was made on an authenticated channel")
  @ApiResponse(responseCode = "404", description = "The payment provider has no purchase with the provided purchaseIdentifier")
  @ApiResponse(responseCode = "409", description = "The purchase was already redeemed for a receipt credential, but with a different receipt credential request")
  @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
      name = "Retry-After",
      description = "If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed"))
  @ManagedAsync
  public Response createLoginReceiptCredential(
      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,
      @NotNull @Valid final CreateLoginReceiptCredentialRequest request)
      throws IOException, SubscriptionPaymentRequiredException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException, SubscriptionReceiptAlreadyRedeemedException {

    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
      throw new BadRequestException("login purchases are not enabled");
    }

    if (authenticatedAccount.isPresent()) {
      throw new ForbiddenException("must not use authenticated connection for login purchase operations");
    }

    final ReceiptCredentialRequest receiptCredentialRequest;
    try {
      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);
    } catch (final InvalidInputException e) {
      throw new BadRequestException("invalid receipt credential request", e);
    }

    try {
      final ReceiptCredentialResponse receiptCredentialResponse = loginPurchaseManager.generateReceipt(
          request.paymentProvider, request.purchaseIdentifier, receiptCredentialRequest);
      return Response.ok(
              new CreateLoginReceiptCredentialResponse(receiptCredentialResponse.serialize()))

View on GitHub (pinned to 100ab61c82)