signalapp/Signal-Server · error · BackupBadReceiptException
receipt credential presentation verification failed
Error message
receipt credential presentation verification failed
What it means
redeemReceipt verifies a ZK receipt credential presentation against the server's ZK receipt operations. If cryptographic verification fails (VerificationFailedException), the receipt is deemed invalid and a BackupBadReceiptException is thrown; the client's presentation does not match what the server can verify.
Solutions
- Re-purchase or re-fetch the receipt and rebuild the presentation from the original receipt credential.
- Verify the client is talking to the same environment/issuer keys that issued the receipt.
- Check that the presentation bytes survive round-trips intact (no line wrapping, correct base64 variant).
- Catch BackupBadReceiptException and surface a 'receipt invalid, restore from purchase' flow instead of retrying with the same bytes.
Example fix
// before
try {
backupAuthManager.redeemReceipt(account, presentation);
} catch (Exception e) {
retry();
}
// after
try {
backupAuthManager.redeemReceipt(account, presentation);
} catch (BackupBadReceiptException e) {
promptUserToReacquireReceipt();
} Defensive patterns
Strategy: try-catch
Validate before calling
// No cheap client-side check; verify the presentation bytes round-trip losslessly before sending
byte[] canonical = Base64.getDecoder().decode(Base64.getEncoder().encodeToString(presentationBytes));
if (!Arrays.equals(canonical, presentationBytes)) throw new IllegalStateException("presentation bytes corrupted"); Type guard
boolean isWellFormedPresentation(byte[] bytes) {
return bytes != null && bytes.length > 0;
} Try / catch
try {
backupAuthManager.redeemReceipt(account, presentation);
} catch (BackupBadReceiptException e) {
if (e.getMessage().contains("verification failed")) {
promptUserToRestorePurchase();
}
} Prevention
- Store and transmit receipt presentations as opaque bytes with checksum verification
- Keep issuer keys consistent between environments; never mix staging receipts with production servers
- Do not retry redemption with the same failing presentation; reacquire the receipt instead
- Log only the exception message, never the presentation material
When it happens
Trigger: Calling redeemReceipt with a receipt credential presentation that was forged, corrupted, generated against a different issuer key, or truncated/mis-serialized during client storage.
Common situations: Client using receipts issued by a different environment (staging vs production issuer keys); downgraded/rotated server ZK keys; byte corruption or base64 mangling of the stored presentation; replaying a modified presentation.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Only primary device can set backup-id
- Must set at least one of message/media credential requests
- Invalid sourceObject size
- unknown cdn
- backup auth credential presentation verification failed
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/8b0e4e25da58a7f3.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java:250
}
}
return credentials;
}
/**
* Redeem a receipt to enable paid backups on the account.
*
* @param account The account to enable backups on
* @param receiptCredentialPresentation A ZK receipt presentation proving payment
*/
public void redeemReceipt(
final Account account,
final ReceiptCredentialPresentation receiptCredentialPresentation)
throws BackupBadReceiptException, BackupInvalidArgumentException, BackupMissingIdCommitmentException {
try {
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
} catch (VerificationFailedException e) {
throw new BackupBadReceiptException("receipt credential presentation verification failed");
}
final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();
final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
if (clock.instant().isAfter(receiptExpiration)) {
throw new BackupBadReceiptException("receipt is already expired");
}
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.PAID) {
throw new BackupInvalidArgumentException("server does not recognize the requested receipt level");
}
if (account.getBackupCredentialRequest(BackupCredentialType.MEDIA).isEmpty()) {
throw new BackupMissingIdCommitmentException();
}
boolean receiptAllowed = redeemedReceiptsManagerView on GitHub (pinned to 100ab61c82)