{"record":{"id":"d166f3a6773c29e6","repo":"signalapp/Signal-Server","slug":"invalid-receipt-level","errorCode":null,"errorMessage":"Invalid receipt level","messagePattern":"Invalid receipt level","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":361,"sourceCode":"      receiptCredentialPresentation = receiptCredentialPresentationFactory\n          .build(registrationRequest.receiptCredentialPresentation());\n    } catch (InvalidInputException _) {\n      throw new BadRequestException(\"Invalid receipt credential presentation\");\n    }\n    try {\n      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);\n    } catch (VerificationFailedException _) {\n      throw new NotAuthorizedException(\"Receipt credential presentation verification failed\");\n    }\n\n    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());\n    if (clock.instant().isAfter(receiptExpiration)) {\n      throw new NotAuthorizedException(\"Receipt is already expired\");\n    }\n\n    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();\n    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {\n      throw new BadRequestException(\"Invalid receipt level\");\n    }\n\n    try {\n      final Account account = accounts.create(\n          registrationRequest.accountAttributes(),\n          registrationRequest.aciIdentityKey(),\n          receiptCredentialPresentation,\n          new DeviceSpec(\n              registrationRequest.accountAttributes().getName(),\n              password,\n              signalAgent,\n              registrationRequest.accountAttributes().getCapabilities(),\n              new DeviceIdentityInfo(registrationRequest.accountAttributes().getRegistrationId(), registrationRequest.deviceActivationRequest()\n                  .aciSignedPreKey(), registrationRequest.deviceActivationRequest().aciPqLastResortPreKey()),\n              Optional.empty(),\n              registrationRequest.accountAttributes().getFetchesMessages(),\n              registrationRequest.deviceActivationRequest().apnToken(),\n              registrationRequest.deviceActivationRequest().gcmToken()),","sourceCodeStart":343,"sourceCodeEnd":379,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L343-L379","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nReceiptCredentialRequestContext ctx = requestReceipt(ReceiptLevel.DONATION, ...);\n// after\nReceiptCredentialRequestContext ctx = requestReceipt(ReceiptLevel.LOGIN, ...);","handlingStrategy":"validation","validationCode":"if (presentation.getReceiptLevel() != ReceiptLevel.LOGIN.getValue()) {\n  throw new IllegalStateException(\"receipt level \" + presentation.getReceiptLevel() + \" is not LOGIN\");\n}","typeGuard":"boolean isLoginReceipt(ReceiptCredentialPresentation p) {\n  return p.getReceiptLevel() == ReceiptLevel.LOGIN.getValue();\n}","tryCatchPattern":"try {\n  register(request);\n} catch (BadRequestException e) {\n  if (e.getMessage().contains(\"Invalid receipt level\")) {\n    repurchaseAtLevel(ReceiptLevel.LOGIN);\n  } else throw e;\n}","preventionTips":["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."],"tags":["zk-receipt","registration","bad-request","receipt-level"],"backgroundTag":"invalid-enum-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"}