signalapp/Signal-Server · error · BadRequestException
Invalid receipt credential presentation
Error message
Invalid receipt credential presentation
What it means
registerAccountWithoutNumber parses the receiptCredentialPresentation bytes supplied in the RegistrationRequest through ReceiptCredentialPresentationFactory. If parsing fails (InvalidInputException) the client supplied malformed, truncated, or wrong-type data, so the controller throws a BadRequestException (HTTP 400). This happens before any cryptographic verification — the presentation could not even be deserialized.
Solutions
- Regenerate the ReceiptCredentialPresentation client-side from the original receipt credential with a current, matching zk-credentials library version.
- Verify the presentation bytes are passed through unmodified (no base64 double-encoding, truncation, or re-serialization) between client and server.
- Check client and server are using the same ZK receipt system (same ReceiptCredentialPresentation schema/generation).
- Log/inspect the raw presentation length before sending to catch empty or truncated payloads.
Example fix
// before: sending a stale/corrupted blob
request.setReceiptCredentialPresentation(oldBytes);
// after: rebuild from the verified receipt credential
ReceiptCredentialPresentation p = clientZkReceiptOperations
.createReceiptCredentialPresentation(receiptSecretParams, receiptCredential);
request.setReceiptCredentialPresentation(p.serialize()); Defensive patterns
Strategy: validation
Validate before calling
byte[] presentation = request.receiptCredentialPresentation();
if (presentation == null || presentation.length == 0) {
throw new IllegalArgumentException("receiptCredentialPresentation missing/empty");
}
// dry-run parse locally with the same factory/version before sending
new ReceiptCredentialPresentation(presentation); Type guard
boolean isUsablePresentation(byte[] b) {
try { new ReceiptCredentialPresentation(b); return true; }
catch (InvalidInputException e) { return false; }
} Try / catch
try {
register(request);
} catch (BadRequestException e) {
if (e.getMessage().contains("Invalid receipt credential presentation")) {
regeneratePresentationAndRetryOnce();
} else throw e;
} Prevention
- Pin matching zk-credentials/libsignal versions on client and server.
- Never re-encode or truncate serialized presentation bytes.
- Round-trip parse the presentation locally before network submission.
When it happens
Trigger: POST to the registration endpoint with receiptCredentialPresentation bytes that are not a valid ZK receipt credential presentation: corrupted base64/byte encoding, a presentation from a different ZK system/version, or random/garbage bytes in the field.
Common situations: Client built the presentation with a mismatched libsignal/zkcredential version; receipt data truncated in transit or in local storage; developer testing the login-purchase endpoint with hand-crafted or placeholder receipt payloads.
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
- Invalid receipt level
- invalid receipt credential request
- login purchases are not enabled
- Receipt credential presentation verification failed
- Receipt is already expired
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/7986a2061348f1f8.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java:346
final String password,
final RegistrationRequest registrationRequest,
final String userAgent,
final String signalAgent) {
if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
throw new BadRequestException("login purchases are not enabled");
}
registrationRequest.accountAttributes().recoveryPassword()
.filter(ArrayUtils::isNotEmpty)
.orElseThrow(() -> new WebApplicationException("Account recovery password is required", 422));
final ReceiptCredentialPresentation receiptCredentialPresentation;
try {
receiptCredentialPresentation = receiptCredentialPresentationFactory
.build(registrationRequest.receiptCredentialPresentation());
} catch (InvalidInputException _) {
throw new BadRequestException("Invalid receipt credential presentation");
}
try {
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
} catch (VerificationFailedException _) {
throw new NotAuthorizedException("Receipt credential presentation verification failed");
}
final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
if (clock.instant().isAfter(receiptExpiration)) {
throw new NotAuthorizedException("Receipt is already expired");
}
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {
throw new BadRequestException("Invalid receipt level");
}
try {View on GitHub (pinned to 100ab61c82)