{"record":{"id":"40f743b13425a415","repo":"signalapp/Signal-Server","slug":"invalid-receipt-credential-request","errorCode":null,"errorMessage":"invalid receipt credential request","messagePattern":"invalid receipt credential request","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java","lineNumber":119,"sourceCode":"  @ManagedAsync\n  public Response createLoginReceiptCredential(\n      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,\n      @NotNull @Valid final CreateLoginReceiptCredentialRequest request)\n      throws IOException, SubscriptionPaymentRequiredException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException, SubscriptionReceiptAlreadyRedeemedException {\n\n    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {\n      throw new BadRequestException(\"login purchases are not enabled\");\n    }\n\n    if (authenticatedAccount.isPresent()) {\n      throw new ForbiddenException(\"must not use authenticated connection for login purchase operations\");\n    }\n\n    final ReceiptCredentialRequest receiptCredentialRequest;\n    try {\n      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);\n    } catch (final InvalidInputException e) {\n      throw new BadRequestException(\"invalid receipt credential request\", e);\n    }\n\n    try {\n      final ReceiptCredentialResponse receiptCredentialResponse = loginPurchaseManager.generateReceipt(\n          request.paymentProvider, request.purchaseIdentifier, receiptCredentialRequest);\n      return Response.ok(\n              new CreateLoginReceiptCredentialResponse(receiptCredentialResponse.serialize()))\n          .build();\n    } catch (SubscriptionReceiptRequestedForOpenPaymentException e) {\n      return Response.noContent().build();\n    } catch (VerificationFailedException e) {\n      throw new BadRequestException(\"receipt credential request failed verification\", e);\n    }\n  }\n}\n","sourceCodeStart":101,"sourceCodeEnd":135,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java#L101-L135","documentation":"The server could not deserialize the client-supplied receiptCredentialRequest bytes into a valid ReceiptCredentialRequest. This is thrown as a BadRequestException (HTTP 400) when the zk receipt credential request fails parsing with InvalidInputException. It means the bytes are malformed, not merely that purchase verification failed.","triggerScenarios":"POST to the login purchase receipt endpoint with a body whose receiptCredentialRequest field is not a validly serialized ReceiptCredentialRequest (e.g. truncated, corrupted, or random bytes).","commonSituations":"Client library version mismatch or protocol changes; byte array mangled by encoding (base64 vs raw bytes, JSON escaping); manually constructed or hand-copied request bytes; buggy client code writing to the wrong field.","solutions":["Regenerate the ReceiptCredentialRequest with a current, matching version of libsignal and ensure it is serialized correctly before sending.","Check the client is sending the raw serialized bytes in request.receiptCredentialRequest, not a base64/hex string or re-encoded copy.","Confirm the endpoint and request body shape match the current Signal API (CreateLoginReceiptCredentialRequest)."],"exampleFix":"// before\nbyte[] requestBytes = Base64.getDecoder().decode(userSuppliedString); // may be corrupt/misencoded\n// after\nReceiptCredentialRequest rcr = new ReceiptCredentialRequest(requestBytes); // validate client-side before POST\nif (!rcrVerifyOk(rcr)) { throw new IllegalArgumentException(\"malformed receipt credential request\"); }","handlingStrategy":"validation","validationCode":"if (requestBytes == null || requestBytes.length == 0) throw new IllegalArgumentException(\"empty receipt credential request\");\ntry { new ReceiptCredentialRequest(requestBytes); } catch (InvalidInputException e) { throw new IllegalArgumentException(\"malformed receipt credential request\", e); }","typeGuard":null,"tryCatchPattern":"try { /* send request */ } catch (BadRequestException e) { if (e.getMessage().contains(\"invalid receipt credential request\")) { regenerateRequestAndRetry(); } }","preventionTips":["Always generate receipt credential requests with a current libsignal version","Never re-encode serialized bytes (avoid base64/hex round trips unless required)","Validate the request deserializes client-side before POSTing"],"tags":["http-400","serialization","receipt-credentials"],"backgroundTag":"invalid-argument-format","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"}