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
- Generate the receipt credential request with the official client library (zkReceiptOperations / libsignal) rather than hand-constructing bytes
- Check that the client and server use compatible protocol versions for ReceiptCredentialRequest serialization
- 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
- Always build the request with the official client crypto library
- Do not round-trip the bytes through lossy encodings
- Keep client protocol library versions in sync with the server
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
- receipt credential request failed verification
- Invalid receipt credential presentation
- receipt credential presentation verification failed
- account does not have a phone number
- Operation requires unauthenticated access
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)