signalapp/Signal-Server · error · SubscriptionInvalidArgumentsException

transaction is not a consumable one-time purchase

Error message

transaction is not a consumable one-time purchase

What it means

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.

Solutions

  1. Verify the client sends the transactionId of the consumable one-time purchase, not a subscription's originalTransactionId.
  2. Check the product in App Store Connect is still configured as Consumable; re-purchase if the product type changed.
  3. Decode the JWS transaction client-side and assert type == CONSUMABLE before calling the claim API.
  4. If you need to redeem a subscription, use the subscription flow (lookupAndValidateTransaction) instead of claimOneTimePurchase.

Example fix

// before
client.claimOneTimePurchase(subscriptionTransactionId);
// after: client-side check first
if (decodedTransaction.getType() == Type.CONSUMABLE) {
  client.claimOneTimePurchase(decodedTransaction.getTransactionId());
}
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-check
if (decodedJws.getType() != Type.CONSUMABLE) { throw new Error("only consumable one-time purchases can be claimed"); }

Type guard

function isConsumable(tx) { return tx?.getType?.() === 'CONSUMABLE'; }

Try / catch

try {
  api.claimOneTimePurchase(purchaseId);
} catch (SubscriptionInvalidArgumentsException e) {
  showError("This purchase is not a one-time purchase.");
}

Prevention

When it happens

Trigger: Passing a purchaseId/originalTransactionId whose App Store JWS transaction type is AUTO_RENEWABLE_SUBSCRIPTION or NON_CONSUMABLE into the one-time-purchase claim endpoint.

Common situations: 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.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java:169

    final PaymentTime paymentTime = PaymentTime.periodEnds(Instant.ofEpochMilli(tx.transaction().getExpiresDate()));

    return new ReceiptItem(itemId, paymentTime, getLevel(tx.transaction()).getValue());

  }

  @Override
  public Optional<PaymentDetails> claimOneTimePurchase(final String transactionId)
      throws RateLimitExceededException, SubscriptionInvalidArgumentsException {

    final Optional<JWSTransactionDecodedPayload> maybeTransaction =
        appleAppStoreClient.lookupTransaction(transactionId, Tags.of(LOOKUP_TYPE_TAG, "one_time"));
    if (maybeTransaction.isEmpty()) {
      return Optional.empty();
    }
    final JWSTransactionDecodedPayload transaction = maybeTransaction.get();

    if (transaction.getType() != Type.CONSUMABLE) {
      throw new SubscriptionInvalidArgumentsException("transaction is not a consumable one-time purchase");
    }

    final PaymentStatus paymentStatus = transaction.getRevocationDate() == null
        ? PaymentStatus.SUCCEEDED
        : PaymentStatus.FAILED;

    return Optional.of(new PaymentDetails(
        Objects.requireNonNull(transaction.getTransactionId()),
        getLevel(transaction),
        paymentStatus,
        transaction.getPurchaseDate() != null ? Instant.ofEpochMilli(transaction.getPurchaseDate()) : null,
        null));
  }

  private AppleAppStoreDecodedTransaction lookupSubscription(final String originalTransactionId, final Tags tags)
      throws RateLimitExceededException, SubscriptionNotFoundException {
    try {
      return lookupAndValidateTransaction(originalTransactionId, tags);

View on GitHub (pinned to 100ab61c82)