signalapp/Signal-Server · error · BadRequestException

Invalid receipt level

Error message

Invalid receipt level

What it means

The receipt is valid and unexpired, but its receiptLevel does not equal ReceiptLevel.LOGIN. Login-purchase registration only accepts receipts at the LOGIN level; a receipt purchased at any other level (e.g. a donation or different tier) is rejected with a BadRequestException (HTTP 400).

Solutions

  1. Purchase a new receipt specifying ReceiptLevel.LOGIN when creating the receipt credential, then rebuild and submit the presentation.
  2. Audit the client purchase code so the requested ReceiptLevel matches the endpoint being redeemed.
  3. Do not reuse receipts across product tiers; each level's receipt only redeems on its matching flow.
  4. Verify the server's ReceiptLevel enum matches the client's (version skew can shift values).

Example fix

// before
ReceiptCredentialRequestContext ctx = requestReceipt(ReceiptLevel.DONATION, ...);
// after
ReceiptCredentialRequestContext ctx = requestReceipt(ReceiptLevel.LOGIN, ...);
Defensive patterns

Strategy: validation

Validate before calling

if (presentation.getReceiptLevel() != ReceiptLevel.LOGIN.getValue()) {
  throw new IllegalStateException("receipt level " + presentation.getReceiptLevel() + " is not LOGIN");
}

Type guard

boolean isLoginReceipt(ReceiptCredentialPresentation p) {
  return p.getReceiptLevel() == ReceiptLevel.LOGIN.getValue();
}

Try / catch

try {
  register(request);
} catch (BadRequestException e) {
  if (e.getMessage().contains("Invalid receipt level")) {
    repurchaseAtLevel(ReceiptLevel.LOGIN);
  } else throw e;
}

Prevention

When it happens

Trigger: Redeeming a receipt credential purchased with a receipt level other than ReceiptLevel.LOGIN against the registration endpoint — e.g. client requested the wrong level at purchase time or a receipt from a different product tier is reused.

Common situations: Client bug building the purchase request with the wrong ReceiptLevel; mixing receipts across features (donation receipts vs login receipts); test scripts replaying receipts minted for other endpoints.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:361

      receiptCredentialPresentation = receiptCredentialPresentationFactory
          .build(registrationRequest.receiptCredentialPresentation());
    } catch (InvalidInputException _) {
      throw new BadRequestException("Invalid receipt credential presentation");
    }
    try {
      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
    } catch (VerificationFailedException _) {
      throw new NotAuthorizedException("Receipt credential presentation verification failed");
    }

    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
    if (clock.instant().isAfter(receiptExpiration)) {
      throw new NotAuthorizedException("Receipt is already expired");
    }

    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {
      throw new BadRequestException("Invalid receipt level");
    }

    try {
      final Account account = accounts.create(
          registrationRequest.accountAttributes(),
          registrationRequest.aciIdentityKey(),
          receiptCredentialPresentation,
          new DeviceSpec(
              registrationRequest.accountAttributes().getName(),
              password,
              signalAgent,
              registrationRequest.accountAttributes().getCapabilities(),
              new DeviceIdentityInfo(registrationRequest.accountAttributes().getRegistrationId(), registrationRequest.deviceActivationRequest()
                  .aciSignedPreKey(), registrationRequest.deviceActivationRequest().aciPqLastResortPreKey()),
              Optional.empty(),
              registrationRequest.accountAttributes().getFetchesMessages(),
              registrationRequest.deviceActivationRequest().apnToken(),
              registrationRequest.deviceActivationRequest().gcmToken()),

View on GitHub (pinned to 100ab61c82)