{"record":{"id":"39f774a5c986eb1e","repo":"signalapp/Signal-Server","slug":"receipt-is-already-expired","errorCode":null,"errorMessage":"receipt is already expired","messagePattern":"receipt is already expired","errorType":"exception","errorClass":"BackupBadReceiptException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java","lineNumber":255,"sourceCode":"  /**\n   * Redeem a receipt to enable paid backups on the account.\n   *\n   * @param account                       The account to enable backups on\n   * @param receiptCredentialPresentation A ZK receipt presentation proving payment\n   */\n  public void redeemReceipt(\n      final Account account,\n      final ReceiptCredentialPresentation receiptCredentialPresentation)\n      throws BackupBadReceiptException, BackupInvalidArgumentException, BackupMissingIdCommitmentException {\n    try {\n      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);\n    } catch (VerificationFailedException e) {\n      throw new BackupBadReceiptException(\"receipt credential presentation verification failed\");\n    }\n    final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();\n    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());\n    if (clock.instant().isAfter(receiptExpiration)) {\n      throw new BackupBadReceiptException(\"receipt is already expired\");\n    }\n\n    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();\n\n    if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.PAID) {\n      throw new BackupInvalidArgumentException(\"server does not recognize the requested receipt level\");\n    }\n\n    if (account.getBackupCredentialRequest(BackupCredentialType.MEDIA).isEmpty()) {\n      throw new BackupMissingIdCommitmentException();\n    }\n\n    boolean receiptAllowed = redeemedReceiptsManager\n        .put(receiptSerial, receiptExpiration, receiptLevel, account.getAccountIdentifier());\n    if (!receiptAllowed) {\n      throw new BackupBadReceiptException(\"receipt serial is already redeemed\");\n    }\n    extendBackupVoucher(account, new Account.BackupVoucher(receiptLevel, receiptExpiration));","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java#L237-L273","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nredeemReceipt(presentationFromLastYear); // 400: receipt is already expired\n// after\nif (Instant.now().isBefore(Instant.ofEpochSecond(presentation.getReceiptExpirationTime()))) {\n  redeemReceipt(presentation);\n} else {\n  purchaseAndRedeemNewReceipt();\n}","handlingStrategy":"validation","validationCode":"Instant expiration = Instant.ofEpochSecond(presentation.getReceiptExpirationTime());\nif (!Instant.now().isBefore(expiration)) {\n  throw new IllegalStateException(\"receipt expired \" + expiration + \", purchase a new one\");\n}","typeGuard":null,"tryCatchPattern":"try { redeemReceipt(presentation); }\ncatch (BackupBadReceiptException e) {\n  if (e.getMessage().contains(\"already expired\")) { purchaseNewReceipt(); } else { throw e; }\n}","preventionTips":["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"],"tags":["zk-receipt","backup-auth","expiration"],"backgroundTag":"jwt-token-expired","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}