{"record":{"id":"e6501a34d3c4fa5e","repo":"signalapp/Signal-Server","slug":"transaction-is-not-a-consumable-one-time-purchase","errorCode":null,"errorMessage":"transaction is not a consumable one-time purchase","messagePattern":"transaction is not a consumable one-time purchase","errorType":"exception","errorClass":"SubscriptionInvalidArgumentsException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java","lineNumber":169,"sourceCode":"    final PaymentTime paymentTime = PaymentTime.periodEnds(Instant.ofEpochMilli(tx.transaction().getExpiresDate()));\n\n    return new ReceiptItem(itemId, paymentTime, getLevel(tx.transaction()).getValue());\n\n  }\n\n  @Override\n  public Optional<PaymentDetails> claimOneTimePurchase(final String transactionId)\n      throws RateLimitExceededException, SubscriptionInvalidArgumentsException {\n\n    final Optional<JWSTransactionDecodedPayload> maybeTransaction =\n        appleAppStoreClient.lookupTransaction(transactionId, Tags.of(LOOKUP_TYPE_TAG, \"one_time\"));\n    if (maybeTransaction.isEmpty()) {\n      return Optional.empty();\n    }\n    final JWSTransactionDecodedPayload transaction = maybeTransaction.get();\n\n    if (transaction.getType() != Type.CONSUMABLE) {\n      throw new SubscriptionInvalidArgumentsException(\"transaction is not a consumable one-time purchase\");\n    }\n\n    final PaymentStatus paymentStatus = transaction.getRevocationDate() == null\n        ? PaymentStatus.SUCCEEDED\n        : PaymentStatus.FAILED;\n\n    return Optional.of(new PaymentDetails(\n        Objects.requireNonNull(transaction.getTransactionId()),\n        getLevel(transaction),\n        paymentStatus,\n        transaction.getPurchaseDate() != null ? Instant.ofEpochMilli(transaction.getPurchaseDate()) : null,\n        null));\n  }\n\n  private AppleAppStoreDecodedTransaction lookupSubscription(final String originalTransactionId, final Tags tags)\n      throws RateLimitExceededException, SubscriptionNotFoundException {\n    try {\n      return lookupAndValidateTransaction(originalTransactionId, tags);","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java#L151-L187","documentation":"claimOneTimePurchase only accepts App Store transactions whose decoded payload type is CONSUMABLE (a one-time purchase). If the JWS transaction's type field is anything else (e.g. AUTO_RENEWABLE, NON_CONSUMABLE), Signal throws SubscriptionInvalidArgumentsException because the claim path is designed exclusively for consumable one-time purchases like one-time donations.","triggerScenarios":"Passing a purchaseId/originalTransactionId whose App Store JWS transaction type is AUTO_RENEWABLE_SUBSCRIPTION or NON_CONSUMABLE into the one-time-purchase claim endpoint.","commonSituations":"Client accidentally sends a subscription transactionId instead of the consumable purchase's transactionId; a product was reconfigured in App Store Connect from consumable to non-consumable or subscription.","solutions":["Verify the client sends the transactionId of the consumable one-time purchase, not a subscription's originalTransactionId.","Check the product in App Store Connect is still configured as Consumable; re-purchase if the product type changed.","Decode the JWS transaction client-side and assert type == CONSUMABLE before calling the claim API.","If you need to redeem a subscription, use the subscription flow (lookupAndValidateTransaction) instead of claimOneTimePurchase."],"exampleFix":"// before\nclient.claimOneTimePurchase(subscriptionTransactionId);\n// after: client-side check first\nif (decodedTransaction.getType() == Type.CONSUMABLE) {\n  client.claimOneTimePurchase(decodedTransaction.getTransactionId());\n}","handlingStrategy":"validation","validationCode":"// client-side pre-check\nif (decodedJws.getType() != Type.CONSUMABLE) { throw new Error(\"only consumable one-time purchases can be claimed\"); }","typeGuard":"function isConsumable(tx) { return tx?.getType?.() === 'CONSUMABLE'; }","tryCatchPattern":"try {\n  api.claimOneTimePurchase(purchaseId);\n} catch (SubscriptionInvalidArgumentsException e) {\n  showError(\"This purchase is not a one-time purchase.\");\n}","preventionTips":["Decode the JWS transaction client-side and assert type == CONSUMABLE before submitting","Keep App Store Connect product types stable; never convert consumables to subscriptions","Route subscription transactions to the subscription flow, not the one-time-purchase claim flow"],"tags":["app-store","one-time-purchase","transaction-type","argument-validation"],"backgroundTag":"invalid-argument-value","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"}