signalapp/Signal-Server · error · BackupPermissionException
credential does not support the requested operation
Error message
credential does not support the requested operation
What it means
BackupPermissionException thrown by BackupManager.checkBackupLevel when the authenticated backup user's backupLevel is lower than the level the operation requires. FREE-level credentials cannot perform operations gated on BackupLevel.PAID (such as media copies). An authorization-failure counter with a 'level' reason tag is incremented.
Solutions
- Redeem a fresh paid receipt to extend the backup voucher and re-authenticate to get a PAID credential
- Route the operation through the FREE-tier API surface if paid features are not needed
- Detect the level authorization error client-side and pause paid-tier sync until the subscription is renewed
- Verify the redeemed receiptLevel actually maps to BackupLevel.PAID before expecting paid operations to succeed
Example fix
// before
copyMedia(freeBackupUser, toCopy); // BackupPermissionException
// after
if (backupUser.backupLevel().compareTo(BackupLevel.PAID) >= 0) {
copyMedia(backupUser, toCopy);
} else {
redeemPaidReceiptThenRetry();
} Defensive patterns
Strategy: validation
Validate before calling
if (backupUser.backupLevel().compareTo(BackupLevel.PAID) < 0) {
throw new IllegalStateException("operation requires PAID backup level");
} Try / catch
try { copyMedia(backupUser, toCopy); }
catch (BackupPermissionException e) {
if (e.getMessage().contains("does not support")) { pausePaidSyncUntilRenewal(); } else { throw e; }
} Prevention
- Check the authenticated backup level before calling paid-tier endpoints
- Re-authenticate after redeeming receipts so credentials reflect the new level
- Handle subscription lapse by downgrading client behavior to FREE operations
- Gate paid features on the credential's actual level, not purchase state alone
When it happens
Trigger: Authenticating with a FREE backup credential and then calling PAID-only endpoints — e.g. getCopyQuota/copy media, or any handler that calls checkBackupLevel(user, BackupLevel.PAID) — while the account's voucher expired or the receipt level only grants FREE.
Common situations: Paid subscription lapsing so the account downgraded to FREE while the client keeps issuing paid-tier requests; redeeming a lower receipt level; testing with free-tier credentials against paid endpoints; cached credentials outliving the paid period.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Only primary device can set backup-id
- wrong credential type for the requested operation
- Must set at least one of message/media credential requests
- receipt credential presentation verification failed
- Invalid sourceObject size
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/4ea1e8f4015875a2.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java:782
}
/**
* Check that the authenticated backup user is authorized to use the provided backupLevel
*
* @param backupUser The backup user to check
* @param backupLevel The authorization level to verify the backupUser has access to
* @throws BackupPermissionException if the backupUser is not authorized to access {@code backupLevel}
*/
@VisibleForTesting
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel)
throws BackupPermissionException {
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(backupUser.userAgent()),
Tag.of(FAILURE_REASON_TAG_NAME, "level")))
.increment();
throw new BackupPermissionException("credential does not support the requested operation");
}
}
/**
* Check that the authenticated backup user is authenticated with the given credential type
*
* @param backupUser The backup user to check
* @param credentialType The credential type to require
* @throws BackupWrongCredentialTypeException error if the backup user is not authenticated with the given
* {@code credentialType}
*/
@VisibleForTesting
static void checkBackupCredentialType(final AuthenticatedBackupUser backupUser, final BackupCredentialType credentialType) throws BackupWrongCredentialTypeException {
if (backupUser.credentialType() != credentialType) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME,
FAILURE_REASON_TAG_NAME, "credential_type")
.increment();
View on GitHub (pinned to 100ab61c82)