The Integration Playbook

Apple's New Plans Can Silently Double Your Commission Payouts

Apple's new 12-month commitment plans can quietly trigger the same commission twice if you check purchases yourself. Here's the fix we shipped.

A developer at a dual-monitor desk at night studying a glowing timeline diagram representing a subscription renewal chain.

#What Broke When a Real App Adopted a 12-Month Commitment Plan

We migrate our own apps against InfluTo before we ship anything to customers. Kaloria was the guinea pig for Apple's monthly-subscription-with-12-month-commitment plan type, and it's the app that caught this bug before it reached a single external integration.

The scenario: a subscriber on a 12-month commitment plan hits their third renewal. DID_RENEW fires, as expected. The transaction validates. Our Store-Direct handler processes it as a new commissionable event — because on the surface, it looks like one. New transactionId, new purchase timestamp, new receipt to validate against the App Store Server API.

The problem is that this subscriber already triggered a period-3 milestone bonus once, in an earlier test pass on the same commitment cycle. The naive dedup logic — keyed only on transactionId — had no way to know that. It was about to pay a milestone bonus twice on a subscription that had renewed exactly once. In sandbox, caught before production. In production, that's a support ticket and a clawback conversation with an influencer who did nothing wrong.

#Reading the New Payload: billingPlanType and TransactionCommitmentInfo Field by Field

Apple added two fields to JWSTransactionDecodedPayload — and mirrored versions to JWSRenewalInfoDecodedPayload — specifically to describe commitment-plan billing. They landed in the App Store Server API changelog dated April 27, 2026, alongside equivalents in App Store Server Notifications.

billingPlanType tells you how the subscription bills. For a standard subscription it's absent or defaults to up-front billing. For a commitment plan it's set to monthly — the subscriber is billed monthly, at the monthly rate, for the length of the commitment, rather than paying the full term up front.

TransactionCommitmentInfo is the object that actually tells you where the subscriber is in that commitment. It carries period and duration data you use to figure out "which renewal is this, within which commitment."

Here's a redacted, illustrative reconstruction of what a period-3 renewal transaction looks like — this is not verbatim Apple documentation, just the shape you'll see:

Illustrative JWSTransactionDecodedPayload excerpt (redacted, reconstructed)
FieldExample valueNotes
transactionId"2000000998877"Unique per transaction — changes every renewal
originalTransactionId"2000000112233"Stable across the entire subscription lifetime
billingPlanType"MONTHLY"Marks this as a 12-month commitment plan
commitmentInfo.commitmentPeriodCount3Which billing period within the current commitment
commitmentInfo.commitmentDuration"P12M"Total length of the commitment

The field to fixate on is commitmentPeriodCount. It's the only thing in the payload that tells you where you are in the cycle without re-deriving it from timestamps.

#The Double-Count: How a Renewal Transaction Almost Paid Two Influencers

The mechanism is simple once you see it, and easy to miss if you don't: a DID_RENEW notification for a commitment plan renewal carries a new transactionId but keeps the same originalTransactionId as every prior period in that commitment. That's by design — Apple uses originalTransactionId to link the whole chain of periods together.

Our original Store-Direct dedup logic predated commitment plans entirely. It was built for ordinary auto-renewing subscriptions, where the assumption "one transactionId equals one billable event equals one commission" holds fine. Under that assumption, every renewal transaction that clears validation gets its transactionId added to a seen-set, and the payout pipeline moves on.

Commitment plans break the second half of that assumption without breaking the first. transactionId is still unique per renewal — Apple never reuses it. So the naive key never technically collides on the renewal itself. The collision happens one layer up, in milestone logic: our bonus trigger was watching for "has this originalTransactionId crossed into period 3," and on a retry or a delayed webhook replay, it saw period 3 twice under two different transactionId values, because nothing in our key distinguished "period 3, first delivery" from "period 3, redelivered notification."

Diagram of twelve linked calendar tiles with one highlighted tile showing a branching thread that nearly created a duplicate.

#The Fix: A Compound Dedup Key Using originalTransactionId + CommitmentInfo

The fix is one extra field concatenated into the key we already had. Instead of deduplicating on transactionId alone, or even on originalTransactionId alone (which would wrongly collapse every period into one event), the key becomes originalTransactionId + commitmentPeriodCount.

This works because originalTransactionId identifies the subscription instance, and commitmentPeriodCount identifies where in the 12-period commitment this specific transaction sits. Together they're stable across notification retries and redeliveries, but distinct across genuinely new periods.

The condition, in pseudocode:

key = transaction.originalTransactionId + "-" + transaction.commitmentInfo.commitmentPeriodCount

if key in seenCommitmentEvents:
    skip()  // redelivery or replay of an already-processed period
else:
    seenCommitmentEvents.add(key)
    processRecurringCommission(transaction)
    if isMilestonePeriod(transaction.commitmentInfo.commitmentPeriodCount):
        fireMilestoneBonus(transaction)

The decision tree is short: validate the JWS, extract originalTransactionId and commitmentPeriodCount, build the compound key, check membership, and only then touch the ledger. Everything downstream — recurring commission, milestone check — sits behind that gate, not in front of it.

Dedup Key Simulator

Pick a transaction scenario, then compare a naive transactionId-only dedup key against a compound originalTransactionId + commitmentPeriodCount key to see whether commissions get double-counted.

Dedup key mode
Naive (transactionId) Compound (originalTransactionId + commitmentPeriodCount)
transactionId originalTransactionId type commitmentPeriodCount commission dedupKey status
Correct total (compound key) $0.00
Current mode total $0.00
Overpaid delta $0.00

If you're extending your own Store-Direct handler, don't key on transactionId for any commitment-plan logic. Reserve it for idempotency at the HTTP-delivery layer only — Apple can and does redeliver notifications, and transactionId is fine for catching literal duplicate webhook POSTs. It's the wrong key for "has this billing period already been paid out."

#Milestone Bonuses Across 12 Periods Without Re-Firing

A milestone bonus configured for, say, period 6 of a 12-period commitment has to fire exactly once, tied to commitmentPeriodCount == 6 — not to "the sixth renewal event we happened to process." Those sound identical until a notification gets redelivered or a subscriber upgrades mid-commitment and the event stream gets messier than a clean linear sequence.

Once the compound key is in place, milestone logic becomes a lookup rather than a counter: on each validated transaction, read commitmentPeriodCount, check if it matches a configured milestone period, and fire if — and only if — that compound key hasn't been seen. There's no incrementing counter to drift out of sync with reality, because the period number comes straight from Apple's payload instead of being inferred from how many times your webhook handler has been called.

Across a full 12-period commitment, this means a campaign with milestones at, say, periods 3, 6, and 12 fires those three bonuses exactly three times total, regardless of how many redeliveries or retries happen underneath.

Milestone Trigger Calculator

Plan out a 12-period commitment with up to 5 milestone bonuses and see exactly when each one fires.

12-period renewal timeline
Period Status Amount
Total additional payout across all 12 periods: $0

#Sandbox Verification: What We Tested Before Shipping

We ran the fix against three scenarios in Sandbox before it touched production traffic, simulating twelve renewal events per run to cover the full commitment.

The clean renewal case — twelve sequential periods, no retries — confirms the baseline: one recurring commission and each configured milestone bonus fired exactly once, in order.

The plan-upgrade-mid-commitment case checks what happens when a subscriber changes tier partway through the 12 periods. This matters because an upgrade can affect which originalTransactionId subsequent transactions carry, and we needed to confirm our key logic doesn't silently orphan periods after the switch.

The refund-then-repurchase case is the nastiest one: a refund followed by a new purchase can produce a fresh originalTransactionId entirely. We verified the compound key correctly treats that as a new commitment — new commissions, milestone counters reset — rather than trying to stitch it onto the old chain.

Close-up of a hand circling a node on a printed system diagram with a red pen, laptop blurred in the background.

#If You're on RevenueCat Instead of Store-Direct

Only one validation path can be active per app — that's a hard constraint, not a preference. If your app runs on the RevenueCat webhook path, this entire class of bug is someone else's problem to have already solved: RevenueCat parses and stores billingPlanType, renewalBillingPlanType, and commitmentInfo automatically as of their WWDC26 update, so you're not writing custom backend logic to extract these fields from raw JWS payloads.

That said, "RevenueCat parses the fields" and "your webhook consumer deduplicates on the right key" are two different claims. If you're building milestone logic on top of RevenueCat subscriber attributes, the same principle applies: key on the stable subscription identity plus the commitment period, not on the webhook event ID alone.

Diagram of two parallel validation pipelines, one through a webhook cloud and one through a server icon, converging into a single ledger.

#Transferable Checklist for Store-Direct Teams

Before enabling a 12-month commitment plan on any Store-Direct integration, run through this in order:

  1. Confirm your JWS decoding logic reads billingPlanType and commitmentInfo — don't assume older parsing code picks up new fields silently.
  2. Replace any transactionId-only dedup key used for commission or milestone logic with originalTransactionId + commitmentPeriodCount.
  3. Audit milestone triggers specifically — recurring commission logic is more forgiving of duplicate events than one-time bonus logic.
  4. Sandbox-test a full 12-period sequence, plus a mid-commitment upgrade and a refund-then-repurchase case.
  5. Verify refund/repurchase produces a new originalTransactionId in your test data before assuming milestone counters reset correctly.
  6. If you later migrate to RevenueCat, confirm which path is active — running both simultaneously is what causes double-reporting in the first place.
Does Google Play's subscription API have an equivalent to Apple's TransactionCommitmentInfo?

The verified evidence bank here covers only Apple's App Store Server API additions. If you're running Google Play commitment-style plans, don't assume field parity — check Google's own subscription API documentation directly before porting this dedup logic across platforms.

If a subscriber cancels partway through a 12-month commitment, does an already-paid milestone bonus get clawed back?

This is a policy decision for your own commission rules, not something Apple's payload dictates. The dedup key described here only controls whether a bonus fires, not what happens to it after cancellation.

Do existing subscribers who purchased before the April 2026 API update retroactively get billingPlanType populated?

Nothing in the verified changelog entries indicates retroactive backfill for pre-existing transactions. Treat the fields as present going forward from subscriptions actually enrolled in a commitment plan, and don't build logic that assumes older transaction records will suddenly carry them.

Sources

  1. App Store Server API changelog
  2. App Store Server Notifications changelog
  3. Supporting monthly subscriptions with a 12-month commitment
  4. Transaction data types
  5. WWDC26: What's new for subscription apps
  6. Managing the life cycle of monthly subscriptions with a 12-month commitment
JH
Jan Horák — Founder & engineer of InfluTo

Jan builds InfluTo and uses it for his own app portfolio. He writes about what attribution actually looks like from the webhook logs: what breaks, what converts, and what the SDKs can and can't see.

Read next → Pay Influencers by Revenue? One Purchase Could Pay Two Canonical Truth: Fraud & Click Integrity · 8 min read