signalapp/Signal-Server · error · SubscriptionInvalidArgumentsException
must cancel subscription with storekit before deleting
Error message
must cancel subscription with storekit before deleting
What it means
cancelAllActiveSubscriptions refuses to stop tracking an Apple App Store subscription when the transaction is still alive: its status is neither EXPIRED nor REVOKED and auto-renew is still ON. Apple requires the subscription to be cancelled via StoreKit (App Store) before Signal drops it, otherwise the user would lose server-side tracking of a billing-active subscription.
Solutions
- In the app, cancel the subscription via StoreKit (autoRenewDisabled = true) and wait for renewalInfo to show autoRenewStatus OFF before retrying deletion.
- Point the user to App Store settings (Subscriptions) to cancel manually, then re-run the operation.
- If the transaction should already be dead, re-fetch the latest transaction/signed renewal info — cached data may be stale.
- Handle SubscriptionInvalidArgumentsException in the account-deletion UI by prompting the user to cancel IAP first.
Example fix
// client: cancel via StoreKit before calling server delete
let options = Product.SubscriptionInfo.RenewalInfo...
await product.subscription?.renewalInfo // confirm autoRenewStatus == .off
server.cancelAllActiveSubscriptions(originalTransactionId)
// server: caller should first verify
if (tx.signedTransaction().getStatus() != Status.EXPIRED
&& tx.signedTransaction().getStatus() != Status.REVOKED
&& tx.renewalInfo().getAutoRenewStatus() != AutoRenewStatus.OFF) {
// prompt user to cancel in StoreKit first, do not call cancelAllActiveSubscriptions
} Defensive patterns
Strategy: validation
Validate before calling
RenewalInfo info = fetchRenewalInfo(originalTransactionId);
boolean cancellable = tx.getStatus() == Status.EXPIRED || tx.getStatus() == Status.REVOKED || info.getAutoRenewStatus() == AutoRenewStatus.OFF;
if (!cancellable) { promptUserToCancelInStoreKit(); return; } Try / catch
try {
api.cancelAllActiveSubscriptions(originalTransactionId);
} catch (SubscriptionInvalidArgumentsException e) {
showSubscriptionCancellationInstructions(); // direct user to App Store subscription settings
} Prevention
- Check autoRenewStatus == OFF before requesting account deletion
- Fetch fresh transaction/renewal info rather than relying on cached state
- UI should detect active auto-renewing subscriptions early in deletion flows
When it happens
Trigger: Calling account deletion / cancel-all-subscriptions flow with an originalTransactionId whose latest transaction status is ACTIVE/GRACE_PERIOD and whose renewalInfo autoRenewStatus is not OFF.
Common situations: User requests account deletion while their Signal subscription is still auto-renewing through the App Store; migration scripts or cleanup jobs run against active subscriptions.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- transaction is not a consumable one-time purchase
- transactionId did not include a paid subscription or the…
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/e3c410c29fc40418.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java:105
* <p>
* The App Store does not support backend cancellation, so this does not actually cancel, but it does verify that the
* user has no active subscriptions. End-users must cancel their subscription directly through storekit before calling
* this method.
*
* @param originalTransactionId The originalTransactionId associated with the subscription
* @throws RateLimitExceededException If rate-limited
* @throws SubscriptionInvalidArgumentsException If the transaction is valid but does not contain a subscription, or
* the transaction has not already been cancelled with storekit
*/
@Override
public void cancelAllActiveSubscriptions(String originalTransactionId)
throws SubscriptionInvalidArgumentsException, RateLimitExceededException {
try {
final AppleAppStoreDecodedTransaction tx = lookupSubscription(originalTransactionId, Tags.of(LOOKUP_TYPE_TAG, "cancel"));
if (tx.signedTransaction().getStatus() != Status.EXPIRED &&
tx.signedTransaction().getStatus() != Status.REVOKED &&
tx.renewalInfo().getAutoRenewStatus() != AutoRenewStatus.OFF) {
throw new SubscriptionInvalidArgumentsException("must cancel subscription with storekit before deleting");
}
} catch (SubscriptionNotFoundException _) {
// If the subscription is not found there is no need to do anything, so we can squash it
}
// The subscription will not auto-renew, so we can stop tracking it
}
@Override
public SubscriptionInformation getSubscriptionInformation(final String originalTransactionId)
throws RateLimitExceededException, SubscriptionNotFoundException {
final AppleAppStoreDecodedTransaction tx = lookupSubscription(originalTransactionId, Tags.of(LOOKUP_TYPE_TAG, "info"));
final SubscriptionStatus status = switch (tx.signedTransaction().getStatus()) {
case ACTIVE -> SubscriptionStatus.ACTIVE;
case BILLING_RETRY -> SubscriptionStatus.PAST_DUE;
case BILLING_GRACE_PERIOD -> SubscriptionStatus.UNPAID;
case EXPIRED, REVOKED -> SubscriptionStatus.CANCELED;
};
View on GitHub (pinned to 100ab61c82)