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
- Purchase a new receipt specifying ReceiptLevel.LOGIN when creating the receipt credential, then rebuild and submit the presentation.
- Audit the client purchase code so the requested ReceiptLevel matches the endpoint being redeemed.
- Do not reuse receipts across product tiers; each level's receipt only redeems on its matching flow.
- 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
- Always request ReceiptLevel.LOGIN when purchasing receipts for the registration flow.
- Never share receipts across product tiers.
- Assert the embedded receipt level in client tests before submission.
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
- Invalid receipt credential presentation
- server does not recognize the requested receipt level
- login purchases are not enabled
- Receipt credential presentation verification failed
- Receipt is already expired
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)