signalapp/Signal-Server · error · BackupInvalidArgumentException

server does not recognize the requested receipt level

Error message

server does not recognize the requested receipt level

What it means

BackupInvalidArgumentException thrown in redeemReceipt when BackupLevelUtil.fromReceiptLevel(receiptLevel) does not resolve to BackupLevel.PAID. The server only recognizes receipt levels that map to the paid backup tier; any unknown or free-tier level is rejected before a voucher is created. It indicates the credential encodes an entitlement level the server will not honor for backups.

Solutions

  1. Redeem a receipt issued for the paid backup tier (the level sold by the current donation/subscription flow)
  2. Verify client receipt-level constants match the server's BackupLevelUtil mapping
  3. Regenerate the presentation from a fresh purchase rather than a legacy credential
  4. If you are the operator, check that server config/feature flags define the expected backup receipt levels

Example fix

// before
long level = 0; // free/test level
redeemReceipt(presentationWithLevel(level)); // 400: server does not recognize the requested receipt level
// after
long level = PAID_BACKUP_RECEIPT_LEVEL; // level from a real paid purchase
redeemReceipt(presentationWithLevel(level));
Defensive patterns

Strategy: validation

Validate before calling

if (BackupLevelUtil.fromReceiptLevel(presentation.getReceiptLevel()) != BackupLevel.PAID) {
  throw new IllegalStateException("receipt level not valid for paid backups");
}

Try / catch

try { redeemReceipt(presentation); }
catch (BackupInvalidArgumentException e) {
  if (e.getMessage().contains("receipt level")) { purchaseCorrectTierReceipt(); } else { throw e; }
}

Prevention

When it happens

Trigger: Redeeming a ReceiptCredentialPresentation with a receiptLevel that maps to FREE or an unmapped numeric level (e.g. 0, test levels, or levels for other products) instead of the paid backup level.

Common situations: Clients compiled against newer/older receipt-level constants than the server; test or staging credentials used against production; receipts bought for a different subscription product; hand-crafted presentations in integration tests.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/cebd59f5ad3b3603. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java:261

  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));
  }

  /**
   * Extend the duration of the backup voucher on an account.
   *
   * @param account The account to update

View on GitHub (pinned to 100ab61c82)