signalapp/Signal-Server · error · SubscriptionInvalidArgumentsException
transactionId did not include a paid subscription or the…
Error message
transactionId did not include a paid subscription or the provided transactionId was not an originalTransactionId
What it means
lookupAndValidateTransaction fetches the subscription history for the given transactionId and filters for paid transactions matching the sha256 of the originalTransactionId. If none match, Signal cannot key the subscription on an originalTransactionId in SubscriptionManager, so it throws SubscriptionInvalidArgumentsException. Get All Transactions returns any transaction tied to the subscription, which is more permissive than Signal requires.
Solutions
- Send the subscription's originalTransactionId (stable across renewals), not an individual renewal transactionId.
- Confirm the subscription actually had at least one paid transaction (status not in billing-retry/refund).
- Ensure sandbox vs production App Store environment matches where the purchase was made (check the JWS payload's environment field).
- Decode the client's JWS payload and read originalTransactionId directly before submitting it.
Example fix
// before api.lookupAndValidateTransaction(transactionId); // after String originalTx = decodedJwsPayload.getOriginalTransactionId(); api.lookupAndValidateTransaction(originalTx);
Defensive patterns
Strategy: validation
Validate before calling
// send originalTransactionId, extracted from the decoded JWS payload
String originalTx = decodedJws.getOriginalTransactionId();
if (originalTx == null || originalTx.isBlank()) { throw new Error("missing originalTransactionId"); } Type guard
function hasOriginalTransactionId(payload) { return typeof payload?.originalTransactionId === 'string' && payload.originalTransactionId.length > 0; } Try / catch
try {
api.lookupAndValidateTransaction(originalTx);
} catch (SubscriptionInvalidArgumentsException e) {
verifyTransactionHasPaidHistory(); // check sandbox vs prod and paid status
} Prevention
- Always key on originalTransactionId, never a renewal transactionId
- Confirm the subscription has at least one paid (non-refunded, non-retry) transaction
- Match sandbox/production environment between purchase and lookup
When it happens
Trigger: Passing a transactionId that belongs to an unpaid/pending purchase, or an id that is a regular transactionId rather than the originalTransactionId of the subscription, such that the filtered txs list is empty.
Common situations: Client sends the transactionId from a receipt instead of originalTransactionId; refund/failed-renewal means no paid transactions exist; testing with sandbox transaction ids against production.
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
- transaction is not a consumable one-time purchase
- must cancel subscription with storekit before deleting
- unknown payment provider:
- purchase was for an unexpected product
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/ecc9cf0f13948cc5.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java:211
private AppleAppStoreDecodedTransaction lookupAndValidateTransaction(final String originalTransactionId, final Tags errorTags)
throws SubscriptionInvalidArgumentsException, RateLimitExceededException, SubscriptionNotFoundException {
final StatusResponse statuses = appleAppStoreClient.getAllSubscriptions(originalTransactionId, errorTags);
final SubscriptionGroupIdentifierItem item = statuses.getData().stream()
.filter(s -> subscriptionGroupId.equals(s.getSubscriptionGroupIdentifier())).findFirst()
.orElseThrow(() -> new SubscriptionInvalidArgumentsException("transaction did not contain a backup subscription", null));
final List<AppleAppStoreDecodedTransaction> txs = item.getLastTransactions().stream()
.map(txItem -> appleAppStoreClient.verifySubscription(statuses.getEnvironment(), txItem))
.filter(tx -> tx.signedTransaction().getOriginalTransactionId().equals(originalTransactionId))
.filter(decoded -> productIdToLevel.containsKey(decoded.transaction().getProductId()))
.toList();
if (txs.isEmpty()) {
// Get All Subscriptions only requires that the transaction be some transaction associated with the
// subscription. This is too flexible, since we'd like to key on the originalTransactionId in the
// SubscriptionManager.
throw new SubscriptionInvalidArgumentsException("transactionId did not include a paid subscription or the provided transactionId was not an originalTransactionId", null);
}
if (txs.size() > 1) {
logger.warn("Multiple matching product transactions found with a sha256(originalTransactionId)={}, only considering first",
sha256(originalTransactionId));
}
return txs.getFirst();
}
private SubscriptionPrice getSubscriptionPrice(final AppleAppStoreDecodedTransaction tx) {
final BigDecimal amount = new BigDecimal(tx.transaction().getPrice()).scaleByPowerOfTen(-3);
return new SubscriptionPrice(
tx.transaction().getCurrency().toUpperCase(Locale.ROOT),
SubscriptionCurrencyUtil.convertConfiguredAmountToApiAmount(tx.transaction().getCurrency(), amount));
}
private ReceiptLevel getLevel(final JWSTransactionDecodedPayload tx) {
final ReceiptLevel level = productIdToLevel.get(tx.getProductId());View on GitHub (pinned to 100ab61c82)