signalapp/Signal-Server · error · SubscriptionPaymentRequiredException

Cannot acknowledge purchase for subscription in state

Error message

Cannot acknowledge purchase for subscription in state ${subscription.getSubscriptionState()}

What it means

GooglePlayBillingManager.validateToken acknowledges a Play purchase only when the subscription state is ACTIVE, IN_GRACE_PERIOD, or CANCELED (canceled-but-not-yet-expired still grants entitlement). Any other state (e.g. PENDING, IN_GRACE until expiry, PAUSED, ON_HOLD, EXPIRED) raises SubscriptionPaymentRequiredException because the purchase cannot be acknowledged or entitled in that state.

Solutions

  1. Ask the user to fix their payment method in Google Play (the purchase is likely PENDING or ON_HOLD), then resubmit the token.
  2. If the subscription expired, have the user re-subscribe and submit the new purchase token instead of the old one.
  3. Handle SubscriptionPaymentRequiredException in the client by prompting the user to restore/repurchase rather than retrying with the same token.
  4. Query the purchase's state in Play Developer API before calling to pre-check eligibility and give a better error.

Example fix

// before: resubmitting an expired token
client.validateToken(oldExpiredPurchaseToken);
// after: check state and repurchase if needed
if (purchaseState != ACTIVE && purchaseState != IN_GRACE_PERIOD && purchaseState != CANCELED) {
  promptUserToResubscribe();
} else {
  client.validateToken(purchase.getPurchaseToken());
}
Defensive patterns

Strategy: validation

Validate before calling

SubscriptionState state = AcknowledgementState/subscription state from Play API;
if (state != ACTIVE && state != IN_GRACE_PERIOD && state != CANCELED) {
  promptUserToFixPaymentOrResubscribe();
  return;
}

Type guard

function isAcknowledgableState(s) { return ['ACTIVE','IN_GRACE_PERIOD','CANCELED'].includes(s); }

Try / catch

try {
  api.validateToken(purchaseToken);
} catch (SubscriptionPaymentRequiredException e) {
  showPaymentIssueScreen(); // direct user to Play Store payment settings
}

Prevention

When it happens

Trigger: User submits their Play purchase token while the subscription is in PENDING (payment method pending), PAUSED, ON_HOLD (billing declined repeatedly), or fully EXPIRED state.

Common situations: Expired subscriber trying to re-link a stale token; purchase stuck in PENDING because the user's payment method requires action in Play Store; subscription paused/hold due to failed renewals.

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


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java:176

   *                                               user an entitlement
   */
  public ValidatedToken validateToken(String purchaseToken)
      throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {
    final SubscriptionPurchaseV2 subscription = lookupSubscription(purchaseToken);
    final SubscriptionState state = SubscriptionState
        .fromString(subscription.getSubscriptionState())
        .orElse(SubscriptionState.UNSPECIFIED);

    Metrics.counter(VALIDATE_COUNTER_NAME, subscriptionTags(subscription)).increment();

    // We only accept tokens in a state where the user may be entitled to their purchase. This is true even in the
    // CANCELLED state. For example, a user may subscribe for 1 month, then immediately cancel (disabling auto-renew)
    // and then submit their token. In this case they should still be able to retrieve their entitlement.
    // See https://developer.android.com/google/play/billing/integrate#life
    if (state != SubscriptionState.ACTIVE
        && state != SubscriptionState.IN_GRACE_PERIOD
        && state != SubscriptionState.CANCELED) {
      throw new SubscriptionPaymentRequiredException(
          "Cannot acknowledge purchase for subscription in state " + subscription.getSubscriptionState());
    }

    final AcknowledgementState acknowledgementState = AcknowledgementState
        .fromString(subscription.getAcknowledgementState())
        .orElse(AcknowledgementState.UNSPECIFIED);

    final SubscriptionPurchaseLineItem purchase = getLineItem(subscription);
    final ReceiptLevel level = productIdToLevel(purchase.getProductId());

    return new ValidatedToken(level.getValue(), purchase.getProductId(), purchaseToken, requiresAcknowledgement(subscription));
  }


  /**
   * Cancel the subscription. Cancellation stops auto-renewal, but does not refund the user nor cut off access to their
   * entitlement until their current period expires.
   *

View on GitHub (pinned to 100ab61c82)