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
- Redeem a receipt issued for the paid backup tier (the level sold by the current donation/subscription flow)
- Verify client receipt-level constants match the server's BackupLevelUtil mapping
- Regenerate the presentation from a fresh purchase rather than a legacy credential
- 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
- Only redeem receipts from the official paid backup purchase flow
- Keep client receipt-level constants in sync with server BackupLevelUtil
- Never hardcode receipt-level numbers in tests; use the level from a real presentation
- Re-validate tier mapping after client library upgrades
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
- receipt is already expired
- receipt serial is already redeemed
- Invalid receipt level
- Invalid receipt credential presentation
- Receipt credential presentation verification failed
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 updateView on GitHub (pinned to 100ab61c82)