signalapp/Signal-Server · error · BadRequestException

invalid receipt credential request

Error message

invalid receipt credential request

What it means

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.

Solutions

  1. Regenerate the ReceiptCredentialRequest with a current, matching version of libsignal and ensure it is serialized correctly before sending.
  2. Check the client is sending the raw serialized bytes in request.receiptCredentialRequest, not a base64/hex string or re-encoded copy.
  3. Confirm the endpoint and request body shape match the current Signal API (CreateLoginReceiptCredentialRequest).

Example fix

// before
byte[] requestBytes = Base64.getDecoder().decode(userSuppliedString); // may be corrupt/misencoded
// after
ReceiptCredentialRequest rcr = new ReceiptCredentialRequest(requestBytes); // validate client-side before POST
if (!rcrVerifyOk(rcr)) { throw new IllegalArgumentException("malformed receipt credential request"); }
Defensive patterns

Strategy: validation

Validate before calling

if (requestBytes == null || requestBytes.length == 0) throw new IllegalArgumentException("empty receipt credential request");
try { new ReceiptCredentialRequest(requestBytes); } catch (InvalidInputException e) { throw new IllegalArgumentException("malformed receipt credential request", e); }

Try / catch

try { /* send request */ } catch (BadRequestException e) { if (e.getMessage().contains("invalid receipt credential request")) { regenerateRequestAndRetry(); } }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

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

  @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()))
          .build();
    } catch (SubscriptionReceiptRequestedForOpenPaymentException e) {
      return Response.noContent().build();
    } catch (VerificationFailedException e) {
      throw new BadRequestException("receipt credential request failed verification", e);
    }
  }
}

View on GitHub (pinned to 100ab61c82)