Crossdeck Docs
Dashboard

One-off purchases on Apple

Concept Consumables, non-consumables & non-renewing · ~9 min read · Updated June 2, 2026

Consumables and non-consumables are Apple's awkward case. Most guides tell you how to make the purchase and go quiet on the hard part — capturing it reliably and tying it to the right person. This page does the opposite. It explains why one-offs are harder than subscriptions on Apple, and exactly how Crossdeck captures them anyway.

Only sell auto-renewable subscriptions? You can skip this page.

See Subscriptions on Apple instead. One-offs need their own explanation, and it's worth reading before you ship.

The three kinds of one-off purchase

Apple groups three product types under “one-off” (anything that isn't an auto-renewable subscription). They look similar and behave very differently.

Type What it is What it should grant
Consumable Bought and used up, then re-bought — credits, coins, a one-time render. Nothing durable. The grant is spent on use.
Non-consumable A permanent unlock — remove ads, unlock a pro feature forever. Permanent access, restorable across devices.
Non-renewing subscription Time-boxed access that does not auto-renew — a 30-day pass. Access for a fixed period, then it lapses.

The modeling trap to avoid

Never attach a consumable to a permanent entitlement.

The most common one-off mistake — and one some billing tools will let you make silently — is attaching a consumable to a permanent entitlement. Do that and your system reports the entitlement as unlocked forever after a single purchase, even though the thing was meant to be consumed once. The user buys 100 credits once and is marked permanently entitled.

Crossdeck keeps these separate by design (see Entitlements): a consumable purchase is recorded as a purchase, not as a standing entitlement. If you want a consumable to confer durable access, that's a deliberate mapping you make — never an accident of how the purchase was recorded.

Why one-offs are harder than subscriptions

This section is the part other docs leave out. Understanding why one-offs are awkward is what lets you trust that they're handled.

Apple's whole server-side machinery was built for subscriptions, because subscriptions have a problem one-offs don't: they renew when your app isn't open. A monthly subscription charges again three months from now while the app sits closed on the phone — your server has no way to know unless Apple tells it. So Apple built server-to-server notifications and a status API specifically for that off-device lifecycle.

A one-off has none of that. It completes on the device, in a single moment, and Apple hands the app a signed receipt right then. For years, Apple's assumption was simply: the device has the proof, so the app can report it — why would the server need a separate notification? That assumption left four rough edges:

  1. Notifications were the weak path.

    Apple's notification for one-off purchases, ONE_TIME_CHARGE, covers consumables, non-consumables, and non-renewing subscriptions — but it was sandbox-only from June 2024 and only reached production on May 27, 2025. Before that date there was no server notification for one-offs at all. Any integration built on the old assumption could miss them entirely.

  2. Sales reports lag and don't name a person.

    App Store Connect's Sales, Trends, and Payments screens are delayed, aggregated financial reports. They reconcile on Apple's own clock — often a day or more behind — and they don't attribute a purchase to a specific customer. This is almost always why a dashboard's live numbers “don't match Apple Connect”: you're comparing a live transaction ledger against delayed financial reporting. They were never meant to line up in real time.

  3. The transaction API needs a starting point.

    Apple's Get Transaction History endpoint walks a single customer's purchase history — but you have to give it a transaction ID belonging to that customer to start from. It's perfect for refreshing a customer you already know; it can't, by itself, discover a customer you've never seen.

  4. Consumables fall off the receipt.

    Once a consumable transaction is finished, it drops out of the customer's receipt. To attribute it correctly after the fact, you need a stable identifier that ties the purchase to a known user — independent of the receipt.

None of these are bugs in your app. They're the texture of a system that matured around subscriptions and only recently grew up around one-offs.

How Crossdeck captures one-offs

Crossdeck captures one-off purchases through two independent paths that converge on the same record. Neither depends on you writing receipt-validation code, and both reconcile against Apple as the source of truth.

Real-time: the webhook

The moment Apple sends a ONE_TIME_CHARGE notification, Crossdeck verifies its signature and records the purchase. The important detail: Crossdeck routes the purchase by what's inside the transaction — its product type — not by the notification's name. A ONE_TIME_CHARGE carrying a consumable is recognised as a one-off because the transaction says consumable, not because the integration was watching for that specific notification string.

This is why Apple's May 2025 change landed for free.

Crossdeck never depended on the notification label, so the day Apple started emitting one-off notifications in production, they were captured without a code change.

Backstop: reconciliation against Apple's ledger

Real-time delivery is never 100% — a device can be offline, a notification can be missed. So Crossdeck runs a reconciliation pass — on demand from the Refresh control on the Revenue page, and automatically once a day — that re-pulls directly from Apple's ledger via two legs:

Every path — webhook, discovery, deepen — keys on Apple's unique transaction ID, so the same purchase arriving by more than one route collapses to a single record. No double-counting.

Attribution: the right person, even after the receipt clears

When you wire Crossdeck on Apple (see below), each purchase carries an appAccountToken that binds it to the Crossdeck user who made it. That binding is what lets a consumable land on the correct human even after it falls off the receipt — solving rough edge #4 above. The purchase is attributed to a person, not stranded as an anonymous transaction.

The one honest limit

A single narrow case remains — and we'd rather state it than pretend it away.

The discovery leg replays roughly 180 days of notification history; the deepen leg needs a known customer to walk. So: a consumable purchased by a customer Crossdeck has never otherwise seen, where the real-time notification was missed and more than ~180 days have passed. For a live app this is vanishingly rare and shrinks to nothing as long as real-time capture is healthy — but it is the honest floor.

What you wire

One-off capture uses the same two steps as subscriptions — there's nothing extra to learn for one-offs. From the Apple connection flow:

1. Identify the user at sign-in.

Crossdeck.identify(user.id)

Links the signed-in person to a Crossdeck customer.

2. Attach the account token on every StoreKit purchase.

.appAccountToken(Crossdeck.appAccountTokenForCurrentIdentity())

Binds each purchase to the right user across identify(), merges, and SSO upgrades.

That's it. Capture happens on Crossdeck's side via the rail — you don't validate receipts or post transactions yourself. Skip step 2 and purchases still arrive, but they land unattributed in a review queue for you to link by hand — matched or held, never guessed.

Verifying it

Crossdeck treats Apple's ledger as the source of truth, not its own stored copy. The reconciliation pass re-derives your one-off purchases directly from Apple — so the records you see are confirmed against Apple's own books, not just whatever was written at capture time. If a purchase is in Apple's ledger, reconciliation brings it into Crossdeck against the right customer; if Crossdeck shows it, it traces back to an Apple-signed transaction.

To confirm for yourself during integration: make a purchase (see testing below), then open the customer in Crossdeck and check the one-off appears in their Revenue record, attributed to them. Because a consumable grants nothing durable, you should not expect an entitlement to flip — the correct result for a consumable is “purchase recorded, no standing entitlement.” That absence is the right answer, not a missed capture.

Testing before you ship

You do not need your app live on the App Store to test one-off capture. There are three environments, and the difference between them is the single most common source of “why isn't this working” — so it's worth being precise.

Environment Apple in the loop? What it proves App Store submission needed?
Local StoreKit testing (Xcode .storekit file) No — purchases are simulated on your Mac Your SDK wiring, identify, the purchase flow No
Apple Sandbox (dev build, Sandbox Apple ID) Yes — real Apple servers, free purchases The full chain: purchase → Apple → Crossdeck → reconciled No
TestFlight (sandbox backend, tester's Apple ID) Yes The same chain, with a wider tester group No (pre-release)

The key point: one-off capture only reconciles where Apple is actually in the loop — Sandbox or TestFlight, not local StoreKit testing. Local testing fakes the purchase on-device; Apple never sees it, so there's nothing in Apple's ledger for Crossdeck to reconcile against. Local testing is excellent for exercising your purchase flow fast; it just can't prove the Apple → Crossdeck path, because that path isn't exercised.

Apple Sandbox runs entirely before submission.

You install a development build straight from Xcode (or via TestFlight), sign in with a free Sandbox test account created in App Store Connect, and make a free test purchase that genuinely travels to Apple's servers — which is exactly what capture needs.

To get there:

  1. Create the product in App Store Connect and bring it to a Ready to Submit state (a product that isn't won't appear to a tester).
  2. Make sure your Paid Apps (tax/banking) agreement is active — if it isn't, Apple returns no products and the purchase button does nothing.
  3. Create a Sandbox test account under Users and Access → Sandbox Testers.
  4. Sign into that account on the device (iOS 18: Settings → Developer → Sandbox Apple Account).
  5. Run your build, buy the product — the prompt is stamped Environment: Sandbox, and nothing is charged.
Two expected quirks so they don't read as failures.

New products can take up to 24 hours to propagate before they're purchasable, and prices in test environments may not match App Store Connect exactly. Neither indicates a problem with your integration.

Consumable balances

Crossdeck captures and attributes the consumable purchase — who bought what, when, confirmed against Apple. Tracking how a consumable is spent down — a running balance of credits remaining — is application logic today: your app knows when a credit is used in a way Apple and Crossdeck cannot. A verifiable consumable ledger (purchases as grants, app-reported usage as spends, a balance that re-derives from source) is on the Crossdeck roadmap; until then, treat capture and attribution as Crossdeck's job and balance as yours.

Subscriptions, by contrast

If you're wondering whether subscriptions carry any of this complexity — they don't. Subscriptions are Apple's first-class case: robust notifications for years, a dedicated status endpoint, no falling-off-the-receipt problem. What subscriptions need from you isn't careful capture — capture is solved — it's correct handling of their states (trials, renewals, grace periods, billing retry, cancellation-but-still-active). See Subscriptions on Apple for that.

You inherited the mature side of Apple's system for subscriptions and the immature side for one-offs. This page exists because the immature side needed explaining. The capture, on Crossdeck's side, is solved either way.


One-off capture routes by the decoded transaction's product type, reconciles against Apple's ledger as the source of truth, and attributes via appAccountToken. Code references: backend/src/webhooks/apple.ts, backend/src/lib/apple-server-api.ts, backend/src/workers/apple-mrr-reconcile.ts, backend/src/lib/purchase-mapping.ts, backend/src/lib/apple-verifier.ts.