{"record":{"id":"1f2811de9c7f9edc","repo":"signalapp/Signal-Server","slug":"cannot-acknowledge-purchase-for-subscription-in-st","errorCode":null,"errorMessage":"Cannot acknowledge purchase for subscription in state ${subscription.getSubscriptionState()}","messagePattern":"Cannot acknowledge purchase for subscription in state (.+?)","errorType":"exception","errorClass":"SubscriptionPaymentRequiredException","httpStatus":402,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java","lineNumber":176,"sourceCode":"   *                                               user an entitlement\n   */\n  public ValidatedToken validateToken(String purchaseToken)\n      throws RateLimitExceededException, SubscriptionNotFoundException, SubscriptionPaymentRequiredException {\n    final SubscriptionPurchaseV2 subscription = lookupSubscription(purchaseToken);\n    final SubscriptionState state = SubscriptionState\n        .fromString(subscription.getSubscriptionState())\n        .orElse(SubscriptionState.UNSPECIFIED);\n\n    Metrics.counter(VALIDATE_COUNTER_NAME, subscriptionTags(subscription)).increment();\n\n    // We only accept tokens in a state where the user may be entitled to their purchase. This is true even in the\n    // CANCELLED state. For example, a user may subscribe for 1 month, then immediately cancel (disabling auto-renew)\n    // and then submit their token. In this case they should still be able to retrieve their entitlement.\n    // See https://developer.android.com/google/play/billing/integrate#life\n    if (state != SubscriptionState.ACTIVE\n        && state != SubscriptionState.IN_GRACE_PERIOD\n        && state != SubscriptionState.CANCELED) {\n      throw new SubscriptionPaymentRequiredException(\n          \"Cannot acknowledge purchase for subscription in state \" + subscription.getSubscriptionState());\n    }\n\n    final AcknowledgementState acknowledgementState = AcknowledgementState\n        .fromString(subscription.getAcknowledgementState())\n        .orElse(AcknowledgementState.UNSPECIFIED);\n\n    final SubscriptionPurchaseLineItem purchase = getLineItem(subscription);\n    final ReceiptLevel level = productIdToLevel(purchase.getProductId());\n\n    return new ValidatedToken(level.getValue(), purchase.getProductId(), purchaseToken, requiresAcknowledgement(subscription));\n  }\n\n\n  /**\n   * Cancel the subscription. Cancellation stops auto-renewal, but does not refund the user nor cut off access to their\n   * entitlement until their current period expires.\n   *","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java#L158-L194","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ask the user to fix their payment method in Google Play (the purchase is likely PENDING or ON_HOLD), then resubmit the token.","If the subscription expired, have the user re-subscribe and submit the new purchase token instead of the old one.","Handle SubscriptionPaymentRequiredException in the client by prompting the user to restore/repurchase rather than retrying with the same token.","Query the purchase's state in Play Developer API before calling to pre-check eligibility and give a better error."],"exampleFix":"// before: resubmitting an expired token\nclient.validateToken(oldExpiredPurchaseToken);\n// after: check state and repurchase if needed\nif (purchaseState != ACTIVE && purchaseState != IN_GRACE_PERIOD && purchaseState != CANCELED) {\n  promptUserToResubscribe();\n} else {\n  client.validateToken(purchase.getPurchaseToken());\n}","handlingStrategy":"validation","validationCode":"SubscriptionState state = AcknowledgementState/subscription state from Play API;\nif (state != ACTIVE && state != IN_GRACE_PERIOD && state != CANCELED) {\n  promptUserToFixPaymentOrResubscribe();\n  return;\n}","typeGuard":"function isAcknowledgableState(s) { return ['ACTIVE','IN_GRACE_PERIOD','CANCELED'].includes(s); }","tryCatchPattern":"try {\n  api.validateToken(purchaseToken);\n} catch (SubscriptionPaymentRequiredException e) {\n  showPaymentIssueScreen(); // direct user to Play Store payment settings\n}","preventionTips":["Check subscription state via Play Developer API before submitting the token","Handle PENDING/ON_HOLD by prompting the user to fix their payment method","For EXPIRED subscriptions, require a fresh purchase rather than resubmitting the old token"],"tags":["google-play-billing","subscription-state","payment-required","purchase-acknowledgement"],"backgroundTag":"invalid-state-transition","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}