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

  1. Re-purchase or re-fetch the receipt and rebuild the presentation from the original receipt credential.
  2. Verify the client is talking to the same environment/issuer keys that issued the receipt.
  3. Check that the presentation bytes survive round-trips intact (no line wrapping, correct base64 variant).
  4. 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

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


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 = redeemedReceiptsManager

View on GitHub (pinned to 100ab61c82)