signalapp/Signal-Server · error · BackupBadReceiptException
receipt is already expired
Error message
receipt is already expired
What it means
BackupBadReceiptException thrown by BackupAuthManager.redeemReceipt when the ZK receipt credential presentation's receiptExpirationTime is before the server's current clock instant. The library treats receipts as strictly time-bounded entitlements; once the encoded expiration has passed the presentation is refused and no backup voucher is extended. It is a client-data problem: the credential itself is well-formed but stale.
Solutions
- Purchase/redeem a fresh receipt and use its newly issued ReceiptCredentialPresentation
- Before redeeming, decode the presentation and check receiptExpirationTime against current time
- Discard persisted receipt credentials after their expiration instead of retrying them
- If expiration is imminent but not passed, retry immediately and investigate client clock sync
Example fix
// before
redeemReceipt(presentationFromLastYear); // 400: receipt is already expired
// after
if (Instant.now().isBefore(Instant.ofEpochSecond(presentation.getReceiptExpirationTime()))) {
redeemReceipt(presentation);
} else {
purchaseAndRedeemNewReceipt();
} Defensive patterns
Strategy: validation
Validate before calling
Instant expiration = Instant.ofEpochSecond(presentation.getReceiptExpirationTime());
if (!Instant.now().isBefore(expiration)) {
throw new IllegalStateException("receipt expired " + expiration + ", purchase a new one");
} Try / catch
try { redeemReceipt(presentation); }
catch (BackupBadReceiptException e) {
if (e.getMessage().contains("already expired")) { purchaseNewReceipt(); } else { throw e; }
} Prevention
- Check receiptExpirationTime before every redemption attempt
- Never persist and replay receipt credentials past their expiration
- Redeem promptly after purchase instead of hoarding credentials
- Monitor device clock sync for near-expiration receipts
When it happens
Trigger: Client calls the redeem-receipt endpoint with a ReceiptCredentialPresentation whose getReceiptExpirationTime() is in the past relative to the server clock; typically a retry of an old redemption or a saved credential reused after its validity window ended.
Common situations: Replaying a receipt that was already redeemed months earlier; restoring backups with an old exported receipt after the paid period lapsed; device clock skew on the client producing credentials near expiration; testing with fixtures generated long ago.
Related errors
- server does not recognize the requested receipt level
- receipt serial is already redeemed
- Invalid receipt credential presentation
- 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/39f774a5c986eb1e.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java:255
/**
* 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
.put(receiptSerial, receiptExpiration, receiptLevel, account.getAccountIdentifier());
if (!receiptAllowed) {
throw new BackupBadReceiptException("receipt serial is already redeemed");
}
extendBackupVoucher(account, new Account.BackupVoucher(receiptLevel, receiptExpiration));View on GitHub (pinned to 100ab61c82)