signalapp/Signal-Server · error · BadRequestException

invalid receipt credential request

Error message

invalid receipt credential request

What it means

The 'receiptCredentialRequest' field must be a valid, well-formed serialized ReceiptCredentialRequest (a ZK-protocol request produced by the Signal client library). If the bytes fail to parse, the controller rejects the request with a 400 BadRequest. This catches malformed, truncated, or incorrectly encoded client-side credentials before any verification happens.

Solutions

  1. Generate the receipt credential request with the official client library (zkReceiptOperations / libsignal) rather than hand-constructing bytes
  2. Check that the client and server use compatible protocol versions for ReceiptCredentialRequest serialization
  3. Ensure the bytes are correctly encoded/decoded (no truncation, correct base64 handling) before sending

Example fix

// before
byte[] req = Base64.decode(userSuppliedString.substring(0, userSuppliedString.length() - 4));
// after
byte[] req = Base64.decode(userSuppliedString); // complete, unmodified server-issued credential request bytes
Defensive patterns

Strategy: validation

Validate before calling

if (receiptCredentialRequestBytes == null || receiptCredentialRequestBytes.length == 0) {
  throw new IllegalArgumentException("receiptCredentialRequest bytes missing");
}
try { new ReceiptCredentialRequest(receiptCredentialRequestBytes); } catch (InvalidInputException e) { /* regenerate client-side */ }

Type guard

boolean isValidReceiptCredentialRequest(byte[] b) { try { new ReceiptCredentialRequest(b); return true; } catch (InvalidInputException e) { return false; } }

Try / catch

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

Prevention

When it happens

Trigger: POST to the boost donation endpoint where request.receiptCredentialRequest is not a validly serialized ReceiptCredentialRequest — e.g. random bytes, base64/hex mis-encoding, or output from an incompatible libsignal version.

Common situations: Client library version mismatch producing incompatible serialization; manual construction of the request bytes; copying a request credential from a different protocol flow; encoding bugs (missing padding, wrong charset) in the client.

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/885d7bfba2490ce0. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java:384

    if (paymentDetails.status() != PaymentStatus.SUCCEEDED) {
      throw new WebApplicationException(Response.status(Response.Status.PAYMENT_REQUIRED)
          .entity(new CreateBoostReceiptCredentialsErrorResponse(paymentDetails.chargeFailure())).build());
    }

    // The payment was successful, try to issue the receipt credential

    final OneTimeDonationUtil.DonationLevelDetails levelDetails;
    try {
      levelDetails = OneTimeDonationUtil.getLevelDetails(paymentDetails, oneTimeDonationConfiguration);
    } catch (OneTimeDonationUtil.InvalidLevelException _) {
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }

    final ReceiptCredentialRequest receiptCredentialRequest;
    try {
      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);
    } catch (final InvalidInputException e) {
      throw new BadRequestException("invalid receipt credential request", e);
    }
    final Instant paidAt = oneTimeDonationsManager.getPaidAt(request.processor, paymentDetails.id(), paymentDetails.created());
    final Instant expiration = paidAt
        .plus(levelDetails.levelExpiration())
        .truncatedTo(ChronoUnit.DAYS)
        .plus(1, ChronoUnit.DAYS);
    try {
      issuedReceiptsManager.recordOneTimeIssuance(paymentDetails.id(), request.processor,
          receiptCredentialRequest, expiration);
    } catch (WriteConflictException _) {
      throw new WebApplicationException(Response.Status.CONFLICT);
    }
    final ReceiptCredentialResponse receiptCredentialResponse;
    try {
      receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
          receiptCredentialRequest, expiration.getEpochSecond(), levelDetails.level().getValue());
    } catch (final VerificationFailedException e) {
      throw new BadRequestException("receipt credential request failed verification", e);

View on GitHub (pinned to 100ab61c82)