@hyodotdev/openiap-commerce-protocol 0.0.0-bootstrap.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CONVENTION.md +168 -0
  2. package/DESIGN.md +1056 -0
  3. package/README.md +227 -5
  4. package/SPEC.md +1471 -0
  5. package/conformance/index.d.ts +303 -0
  6. package/conformance/index.mjs +2126 -0
  7. package/conformance/mock-provider.mjs +491 -0
  8. package/examples/entitlement-granted-no-subscription.json +12 -0
  9. package/examples/entitlement-revoked.json +21 -0
  10. package/examples/provider-capabilities.json +209 -0
  11. package/examples/store-event-mapping.json +287 -0
  12. package/examples/subscription-canceled.json +22 -0
  13. package/examples/subscription-product-changed.json +30 -0
  14. package/examples/subscription-renewed.json +29 -0
  15. package/examples/verify-purchase-request.json +6 -0
  16. package/examples/verify-purchase-result.json +7 -0
  17. package/generated/bindings/graphql-operations.json +87 -0
  18. package/generated/bindings/http-binding.json +143 -0
  19. package/generated/bindings/introspection-signature.json +320 -0
  20. package/generated/bindings/operations-sdl.json +4 -0
  21. package/generated/bindings/operations.graphql +366 -0
  22. package/generated/commerce-protocol.graphql +1219 -0
  23. package/generated/openapi/commerce-protocol.openapi.json +1413 -0
  24. package/generated/schemas/commerce-event.schema.json +499 -0
  25. package/generated/schemas/commerce-protocol.bundle.schema.json +1576 -0
  26. package/generated/schemas/operations.schema.json +578 -0
  27. package/generated/schemas/primitives.schema.json +101 -0
  28. package/generated/schemas/provider-capabilities.schema.json +205 -0
  29. package/generated/schemas/store-event-mapping.schema.json +211 -0
  30. package/generated/vectors/lifecycle.json +908 -0
  31. package/generated/vectors/operations.json +1122 -0
  32. package/package.json +62 -12
  33. package/schema/01-primitives.graphql +102 -0
  34. package/schema/02-commerce-event.graphql +195 -0
  35. package/schema/03-provider-capabilities.graphql +139 -0
  36. package/schema/04-store-event-mapping.graphql +98 -0
  37. package/schema/05-operations.graphql +461 -0
  38. package/schema/06-compiler-vocabulary.graphql +139 -0
  39. package/schema/07-protocol-metadata.graphql +76 -0
  40. package/src/index.d.ts +63 -0
  41. package/src/index.mjs +121 -0
  42. package/vectors/signatures.json +139 -0
package/DESIGN.md ADDED
@@ -0,0 +1,1056 @@
1
+ # Why the Commerce Protocol Draws Its Boundaries Where It Does
2
+
3
+ Version 1.1, 7 September 2026. A whitepaper for engineers who verify
4
+ purchases on a server: why the OpenIAP Commerce Protocol draws its decision
5
+ boundaries where it does, how to implement those boundaries in a backend, and how specialist products
6
+ connect around them. It
7
+ reports no measured result and has not been peer reviewed. [SPEC.md](SPEC.md)
8
+ is authoritative for the normative wording; this explains the reasoning behind
9
+ it.
10
+
11
+ Published as a PDF at <https://www.openiap.dev/commerce-protocol-rationale.pdf>.
12
+ Released under the MIT License, like the rest of the project. Cite it as:
13
+ OpenIAP contributors, _Why the Commerce Protocol Draws Its Boundaries Where
14
+ It Does_, version 1.1, 7 September 2026.
15
+
16
+ ## Abstract
17
+
18
+ A provider accepting purchase evidence does not authorize a particular user's
19
+ current access. Evidence acceptance, account ownership, subscription
20
+ lifecycle and current entitlement are four different questions, and an
21
+ integration has to keep them apart across store APIs, provider
22
+ implementations, transport bindings and events that arrive late.
23
+
24
+ OpenIAP Commerce Protocol is a vendor-neutral server-side contract that makes
25
+ those boundaries explicit and turns a defined subset of them into checks a
26
+ provider can be run against. GraphQL source defines the structure and
27
+ machine-readable metadata, and the build generates the REST and GraphQL
28
+ bindings, capability discovery and JSON Schemas from it. Normative prose
29
+ defines the behavior, and the conformance vectors encode those rules by hand
30
+ against the generated schemas. That hand step is where drift is most likely
31
+ to enter.
32
+
33
+ The sections below cover the six boundaries, the two authorization roles, the temporal
34
+ meaning of an entitlement snapshot, event identity and recovery, and what the
35
+ contract still leaves to you. What the current checks do and do not establish
36
+ is recorded in the
37
+ [evaluation record](https://github.com/hyodotdev/openiap/blob/main/knowledge/research/commerce-protocol-evaluation.md);
38
+ the open questions and what would settle them are in the
39
+ [research agenda](https://github.com/hyodotdev/openiap/blob/main/knowledge/research/research-agenda.md).
40
+
41
+ ## 1. Five symptoms
42
+
43
+ If you ship subscriptions, some of these have happened to you.
44
+
45
+ A paying customer is locked out because your verification provider was down
46
+ and the code treated "no answer" as "not a valid purchase." A customer turns
47
+ off auto-renewal on Tuesday and loses access on Tuesday, though they paid
48
+ through the end of the month. A webhook arrives late, after the subscription
49
+ it describes has already expired, and your handler opens the gate anyway
50
+ because the payload says `active`. The same webhook arrives twice and the
51
+ customer is credited twice. You move to a different verification provider and
52
+ your event handling breaks, because nothing said what its events were
53
+ supposed to mean.
54
+
55
+ None of these is a payment bug. Every one of them is a boundary that got
56
+ crossed: a question was answered with the answer to a different question.
57
+
58
+ ```mermaid
59
+ flowchart TB
60
+ q1["<b>Q1 · evidence</b> — is this acceptable?<br/><i>a verdict, not authorization</i>"]
61
+ q2["<b>Q2 · account</b> — whose purchase is it?<br/><i>the authenticated backend decides</i>"]
62
+ q3["<b>Q3 · lifecycle</b> — what happened to it?<br/><i>renewal intent is not access</i>"]
63
+ q4["<b>Q4 · access</b> — may this user read it now?<br/><i>true only at an evaluation time</i>"]
64
+ q1 --> q2 --> q3 --> q4
65
+ ```
66
+
67
+ **Figure 1.** Four questions an integration must keep apart. Answering one
68
+ does not answer the next.
69
+
70
+ The stores say as much themselves. Google's lifecycle guidance distinguishes
71
+ cancellation before the end of a paid period from expiration, and describes
72
+ continued access during grace [[3]](#ref-3). Apple defines a notification's
73
+ subscription status relative to the time the notification was signed
74
+ [[4]](#ref-4). What a message asserts and when that assertion applies are two
75
+ different things.
76
+
77
+ The design question behind the specification follows from that: **how can a
78
+ server contract preserve the meaning of purchase verification and current
79
+ entitlements across provider and transport boundaries?** Everything below
80
+ answers it for OpenIAP Commerce Protocol 1.0 [[5]](#ref-5), with IAPKit as
81
+ the reference implementation.
82
+
83
+ The scope is principally auto-renewing subscriptions on Apple and Google
84
+ Play, although the protocol's store identifier space is extensible. The
85
+ contract does not prescribe every store transition, implement payment
86
+ settlement, or amount to a complete commerce platform.
87
+
88
+ ## 2. What this reasoning rests on
89
+
90
+ The boundaries below are not invented here. A decade of results shows
91
+ integrations failing at the seams with the cryptography intact: logic flaws
92
+ at the merchant-cashier boundary let shoppers pay nothing without breaking
93
+ anything [[12]](#ref-12); automatically rewriting apps so on-device checks
94
+ returned success defeated in-app billing [[11]](#ref-11), and a later attack
95
+ reached the same result by instrumenting the running app [[1]](#ref-1);
96
+ flawed third-party payment integrations were traced to SDK design,
97
+ documentation and sample code [[2]](#ref-2), and a decade after the
98
+ cashier-as-a-service results the same class of flaw was still being reported
99
+ [[13]](#ref-13). One result shaped this document's form more than the
100
+ others: Chen et al. found payment-integration requirements a developer
101
+ cannot enforce, because the parameters a check would need are not available
102
+ to the party asked to perform it, and because guidance omits checks that are
103
+ required [[22]](#ref-22). The conclusion we draw from that is ours, not
104
+ theirs: an obligation written only as advice is one an implementation cannot
105
+ be failed against, so this contract states what can be checked.
106
+
107
+ The event rules come from the same place. Repeated messages need
108
+ application-level handling and often remembered state, and an operation that
109
+ is naturally idempotent is a different thing from one made idempotent
110
+ [[6]](#ref-6), [[14]](#ref-14). Idempotence alone still does not resolve
111
+ missing events, stale snapshots or ambiguous ordering, which is why the
112
+ contract addresses those separately.
113
+
114
+ Three of the design choices here have a source, though the choices remain
115
+ ours. Deriving tests from a model rather than from an implementation is an
116
+ established method [[8]](#ref-8), and generating stateful requests from a
117
+ service's own specification finds real defects [[15]](#ref-15) — in that work,
118
+ through server errors. The vectors here carry expected outcomes drawn from the
119
+ normative prose, so they check required behavior rather than only request
120
+ validity or a server error. Those outcomes are reviewed apart from the
121
+ implementations because independent implementations can share an error, which
122
+ is a finding [[9]](#ref-9); reviewing them separately is our response to it,
123
+ not a remedy that paper establishes.
124
+
125
+ And an unavailable verifier and an incomplete
126
+ read get explicit rules rather than being left to each implementer, because
127
+ catastrophic failures concentrate in already-signalled errors that were
128
+ mishandled — 92% against 25% for non-catastrophic ones, in five distributed
129
+ data systems [[10]](#ref-10). That last inference is ours; those are
130
+ different systems.
131
+
132
+ Two comparisons place the work. Bishop et al. wrote a behavioral
133
+ specification of TCP and Sockets and checked real implementations against it
134
+ [[21]](#ref-21) — the same shape as this, except theirs was validated against
135
+ implementations they had not written, while this one is authored alongside
136
+ one of its own. Pact is the contract testing you will compare this to
137
+ [[23]](#ref-23); its contracts are consumer-driven, recorded from what one
138
+ consumer needs, whereas these obligations do not change when a new consumer
139
+ appears. Event interchange has a vendor-neutral envelope of its own
140
+ [[7]](#ref-7), which this contract is not a profile of.
141
+
142
+ ## 3. Problem model and trust boundaries
143
+
144
+ ### 3.1 Actors and observations
145
+
146
+ The model contains four actors: a store, a commerce provider, a developer
147
+ backend, and an application. The store is the source of purchase evidence
148
+ and lifecycle facts. The provider verifies evidence and exposes normalized
149
+ operations and events. The developer backend owns application-account
150
+ authorization and consumes the provider's account-scoped results. The
151
+ application initiates purchases and may request account-free verification.
152
+
153
+ The contract distinguishes a transaction, a subscription, an event, and an
154
+ entitlement. A transaction records an economic occurrence; a subscription
155
+ describes an arrangement; an event records a change or observation; an
156
+ entitlement expresses access at an evaluation time. A verification verdict
157
+ is a separate observation about submitted evidence. These are conceptual
158
+ boundaries, not interchangeable names for one boolean.
159
+
160
+ For discussion, let a verification outcome be one of:
161
+
162
+ ```text
163
+ V(evidence) = Accepted | Rejected | OperationError(code)
164
+ ```
165
+
166
+ This notation summarizes the existing contract; it does not add a wire type.
167
+ `Accepted` and `Rejected` correspond to successful operation results with
168
+ `isValid: true` and `isValid: false`. If the provider cannot obtain a verdict,
169
+ the relevant operation error is `VERIFICATION_FAILED`. Other conditions,
170
+ such as authentication failure or rate limiting, retain their own categories.
171
+ No outcome in this expression independently proves which application
172
+ account owns the purchase.
173
+
174
+ ### 3.2 Fault and adversary assumptions
175
+
176
+ The application may be modified, and a credential shipped inside it may be
177
+ copied. Requests can contain malformed or mismatched evidence. An upstream
178
+ verification call can fail without producing a verdict. Store notifications
179
+ and provider webhooks can be delayed, repeated, or unavailable. A downstream
180
+ consumer may observe a previously correct snapshot after its deadline.
181
+
182
+ The design assumes that the provider and developer backend enforce their
183
+ declared roles and that actual verification establishes the required trust
184
+ in store evidence. Conformance tests can expose specific violations of these
185
+ obligations; they cannot establish that a malicious provider tells the truth
186
+ or that a compromised backend protects its users. Cryptographic primitive
187
+ security, stolen server credentials, and exhaustive store-verifier analysis
188
+ are outside what any of the current checks address.
189
+
190
+ ### 3.3 Decision-preservation obligations
191
+
192
+ Table 1 summarizes the selected obligations from the specification
193
+ [[5]](#ref-5). The specification remains authoritative for their complete
194
+ wording and exceptions.
195
+
196
+ | Boundary | Required distinction | Consequence for an integration |
197
+ | --------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ |
198
+ | Evidence to verdict | Rejection differs from inability to obtain a verdict. | An unavailable verifier cannot supply a negative purchase verdict. |
199
+ | Verdict to account | Verification does not establish a binding. | Account association requires the authenticated backend's decision. |
200
+ | Lifecycle to access | Renewal intent differs from current entitlement. | Disabling renewal does not itself close an unexpired paid gate. |
201
+ | Snapshot to delivery | Evaluation time differs from arrival time. | A delayed grant cannot open access at or after its known expiry. |
202
+ | Repeated delivery to effect | An event identity is stable within its emitter context. | Reprocessing that event must not repeat substantive effects. |
203
+ | Provider to consumer | Compatible semantics need not imply identical identifiers. | Portability comparisons normalize only differences the contract permits. |
204
+
205
+ These distinctions prevent particular category errors. Whether developers
206
+ apply them correctly, and how often existing systems violate them, require
207
+ empirical study beyond the contract's definition.
208
+
209
+ ## 4. Protocol design
210
+
211
+ ### 4.1 Authored contract and generated artifacts
212
+
213
+ The protocol separates structural authoring from behavioral obligations.
214
+ GraphQL source files [[16]](#ref-16) define types, operations, and contract
215
+ metadata; normative prose, using the RFC 2119 keywords [[19]](#ref-19),
216
+ defines their meaning. The build generates the assembled contract, JSON
217
+ Schemas in draft 2020-12 [[17]](#ref-17), operation artifacts, REST and
218
+ GraphQL bindings, OpenAPI 3.1 documentation [[18]](#ref-18), and lifecycle
219
+ vectors. Checks reject drift between
220
+ authored inputs and checked-in outputs.
221
+
222
+ These artifacts are distributable without an OpenIAP account or a central
223
+ runtime service. The portable runner imports no IAPKit implementation and
224
+ uses caller-supplied adapters and validation support. A provider can expose
225
+ supported profiles and bindings through capability discovery. Thus adopting
226
+ the contract does not require adopting IAPKit's storage, deployment model,
227
+ or credential format.
228
+
229
+ Single-source generation reduces opportunities for artifacts to disagree,
230
+ but also creates a shared-fault risk. A mistaken authored rule can propagate
231
+ consistently into every generated artifact. Drift checks and behavioral
232
+ validation therefore answer different questions.
233
+
234
+ ### 4.2 Operations and authorization
235
+
236
+ The six operations are `verifyPurchase`, `subscriptionStatus`,
237
+ `entitlements`, `bindPurchase`, `eraseUser`, and `providerCapabilities`.
238
+ Providers declare their supported profiles and must answer honestly for what
239
+ they declare. A caller can branch on those declarations. A caller that skips
240
+ discovery is not punished for it: an operation from a profile the provider
241
+ serves is not refused on that ground, though it can still fail for any
242
+ ordinary reason, and an operation from an unserved profile returns
243
+ `UNSUPPORTED_PROFILE` rather than a wrong answer.
244
+
245
+ `verifyPurchase` is account-free. It accepts store-discriminated evidence
246
+ and returns the authoritative `isValid` gate; its advisory `state` field
247
+ does not replace that gate. The operation cannot read, select, or mutate
248
+ account state. `bindPurchase`, by contrast, requires the server role,
249
+ associates evidence with the backend's user identity, and does not transfer
250
+ an existing binding. Possession of evidence alone is deliberately
251
+ insufficient authority to bind it.
252
+
253
+ ```mermaid
254
+ flowchart LR
255
+ app["shipped application<br/><i>verification role</i>"]
256
+ be["the caller's authenticated backend<br/><i>server role</i>"]
257
+ prov["commerce provider"]
258
+ app -->|"verifyPurchase<br/>providerCapabilities"| prov
259
+ be -->|"those, plus subscriptionStatus,<br/>entitlements, bindPurchase, eraseUser"| prov
260
+ ```
261
+
262
+ **Figure 2.** What each role may call. A verification credential must never
263
+ reach an account read or mutation, which is what stops a shipped application
264
+ from walking arbitrary user identities.
265
+
266
+ The server-only status and entitlement operations return tokenless views.
267
+ They exclude purchase tokens, signed receipts, store transaction identities,
268
+ and provider-internal record identifiers. Unknown optional information is
269
+ omitted, though GraphQL cannot express omitted-versus-null on a selected
270
+ member, so there it arrives as `null` and a caller normalizes that to absent.
271
+ If an operation cannot provide the complete answer required by the
272
+ contract, it fails rather than presenting a partial read as complete.
273
+
274
+ Authorization for privileged operations precedes operation-input validation,
275
+ including GraphQL variable coercion. A resolver-only check is insufficient
276
+ because coercion can execute first. The specification allows earlier
277
+ transport-shape failures, such as an unparseable document. This ordering
278
+ makes the role boundary observable and testable without making it dependent
279
+ on the implementation's internal call structure.
280
+
281
+ ### 4.3 Temporal entitlement semantics
282
+
283
+ For an event carrying a subscription snapshot, let `s` be its state, `d`
284
+ its optional expiry, and `t` its `processedAt`. The provider evaluates the
285
+ following predicate, restated from specification §2.3:
286
+
287
+ ```text
288
+ A(s, d, t) =
289
+ (s is Active or InGracePeriod)
290
+ and (d is absent or t < d)
291
+ ```
292
+
293
+ For grace, `d` is the grace deadline. At equality the gate is closed. When
294
+ expiry is absent, the predicate follows the state but supplies no deadline
295
+ or general freshness guarantee. The provider places this result in
296
+ `subscription.active`. Consumers read that decision instead of reconstructing
297
+ it from `state` while ignoring `active`.
298
+
299
+ A consumer applying a true snapshot must additionally respect a present
300
+ deadline at arrival and during subsequent use. It can schedule expiry or
301
+ obtain an authoritative status before the deadline. These duties do not
302
+ allow a false snapshot to become true merely because its state label looks
303
+ active. When no deadline is available, freshness requirements need an
304
+ appropriate authoritative status source.
305
+
306
+ Consider the illustrative trace in Figure 3. Times are synthetic and do not
307
+ report a store experiment. Assume a bound subscription, no intervening
308
+ revocation, and a known paid deadline at 10:00.
309
+
310
+ ```mermaid
311
+ flowchart TB
312
+ a["<b>09:00</b> · renewal is disabled, the paid period runs to 10:00<br/><i>access stays open — renewal intent is not access</i>"]
313
+ b["<b>09:30</b> · the provider derives a snapshot: active, expires 10:00<br/><i>the gate is open at this evaluation time</i>"]
314
+ c["<b>10:01</b> · that snapshot reaches the consumer<br/><i>its own deadline has passed, so delivery cannot open access</i>"]
315
+ d["<b>10:02</b> · the same event is delivered again<br/><i>same identity, same expired deadline, no new effect</i>"]
316
+ a --> b --> c --> d
317
+ ```
318
+
319
+ **Figure 3.** A snapshot that was true when it was evaluated does not become
320
+ authorization when it arrives. A correct signature and an originally correct
321
+ `active` value are insufficient to authorize access at a later time.
322
+
323
+ ### 4.4 Events, identity, and recovery
324
+
325
+ The event path is store to provider to developer backend. The protocol
326
+ defines bounded request/response operations for application-facing
327
+ verification and server reads; its GraphQL binding has no Subscription
328
+ root. Device notification delivery remains a developer-backend concern.
329
+
330
+ Provider delivery is duplicate-capable and unordered. An event can be
331
+ accepted zero times if delivery permanently fails or its retry budget is
332
+ exhausted. The emitter preserves event identity and body across retries,
333
+ while transport signing, an HMAC-SHA256 [[20]](#ref-20) over the attempt's
334
+ timestamp and the exact body bytes, uses the current attempt's timestamp.
335
+ Consumers deduplicate within the emitter's identity context and handle
336
+ substantive effects idempotently. Retries do not supply an exactly-once
337
+ guarantee.
338
+
339
+ When a consumer can correlate a stable purchase, an older snapshot cannot
340
+ overwrite newer state. This does not make every effect of an older event
341
+ obsolete. Nor does it create a universal ordering key: a user identity or
342
+ product identity alone may be insufficient, and equal occurrence timestamps
343
+ provide no tiebreaker. A consumer unable to establish current state uses
344
+ the declared status or entitlement operations, or an emitter-specific
345
+ authoritative source if those operations are unavailable.
346
+
347
+ First binding introduces another temporal boundary. Previously unbound
348
+ changes are coalesced into the current gate; an expired historical grant is
349
+ not replayed as a new entitlement. Actionable entitlement events require
350
+ both a user and product identity. These rules connect purchase observation
351
+ to account access without making account association implicit in verification.
352
+
353
+ ### 4.5 Portability and evolution
354
+
355
+ The contract defines compatibility at the level of supported profiles,
356
+ bindings, and observable results. Protocol versions and package-distribution
357
+ versions are distinct. Some value spaces and objects allow compatible
358
+ additions, while closed tokenless results and error envelopes restrict
359
+ what a provider can return.
360
+
361
+ REST and GraphQL need not produce byte-identical documents. Their envelope
362
+ rules and treatment of unknown input members differ, so cross-binding
363
+ comparison must use the defined semantics. It must still detect a binding
364
+ that silently drops required information or changes an operation failure
365
+ into a successful answer.
366
+
367
+ It is worth separating what this project claims from what it does not. The
368
+ individual distinctions are established practice: Google's own lifecycle
369
+ guidance already separates a cancellation from the end of access
370
+ [[3]](#ref-3), and this document does not claim to have introduced that one
371
+ or any of the others.
372
+
373
+ Provider switching has a narrower meaning than automatic migration.
374
+ Consumers may preserve their domain logic under compatible contracts while
375
+ changing credentials and endpoints. Historical data, purchase correlation,
376
+ and overlapping deliveries remain operational responsibilities. Emitter
377
+ identifiers are not globally interchangeable, and this version does not
378
+ guarantee deduplication across a provider cutover.
379
+
380
+ ## 5. Implementation blueprint
381
+
382
+ This section turns the boundaries into a buildable provider design. It is
383
+ non-normative: the module layout, record names, locking strategy, and work
384
+ queues are implementation choices, not new protocol requirements. The wire
385
+ contract remains version 1.0. A service with a transactional database and a
386
+ background worker can realize this layout; separate services are unnecessary.
387
+
388
+ The accompanying [implementation guide](https://openiap.dev/commerce-protocol/implementation)
389
+ provides the build milestones and acceptance checklist, and the
390
+ [local example](https://openiap.dev/commerce-protocol/implementation#local-example)
391
+ exercises the contract without store credentials. The example uses fixture
392
+ evidence; it does not validate a real purchase.
393
+
394
+ ### 5.1 Components and responsibilities
395
+
396
+ ```mermaid
397
+ flowchart TB
398
+ caller["App: verification role<br/>Developer backend: server role"]
399
+ api["REST or GraphQL binding<br/>authorization and input validation"]
400
+ domain["Shared domain handlers<br/>verify, bind, read, erase"]
401
+ adapter["Store adapters<br/>verify evidence and obtain authoritative facts"]
402
+ inbox["Authenticated store inbox<br/>deduplicate and normalize observations"]
403
+ db["One transaction boundary<br/>purchase state + binding + event outbox"]
404
+ worker["Delivery worker<br/>sign, retry, dead-letter"]
405
+ consumer["Developer backend<br/>durable inbox and idempotent effects"]
406
+ caller --> api --> domain
407
+ domain <--> adapter
408
+ adapter --> inbox
409
+ domain --> db
410
+ inbox --> db
411
+ db --> worker --> consumer
412
+ ```
413
+
414
+ **Figure 4.** A possible provider decomposition. Store notifications enter
415
+ through store-specific verification, while outgoing deliveries use the
416
+ protocol's signing contract. Domain handlers are shared by both bindings.
417
+ The diagram names logical responsibilities; it does not require one service
418
+ per box.
419
+
420
+ The store adapter owns store credentials, application and environment checks,
421
+ evidence verification, and the translation from store observations into domain
422
+ facts. It does not decide which app user owns a purchase. The authenticated
423
+ developer backend supplies that association through `bindPurchase`. A provider
424
+ still verifies and scopes the evidenced purchase before accepting a binding.
425
+
426
+ Transport code maps requests onto the same operations and maps results back
427
+ onto the selected binding. The generated OpenAPI document and executable
428
+ GraphQL projection supply the external structure. The offline JSON Schema
429
+ bundle supplies structural validators. Neither the schema nor a JSON-valid
430
+ receipt establishes authenticity; that work stays in the store adapter.
431
+
432
+ For server operations, role checks precede operation-input validation. A
433
+ GraphQL implementation checks all executable root fields, including aliases
434
+ and fragments, before variable coercion; it can instead reject document
435
+ shapes it does not support without executing them, while still accepting the
436
+ canonical documents. Authorizing one resolver or trusting a client-supplied
437
+ `operationName` is not a sufficient boundary.
438
+
439
+ ### 5.2 Persistence model and invariants
440
+
441
+ Every private key below includes the provider's trusted scope: tenant or
442
+ project, and application and store environment where applicable. That scope
443
+ comes from credential and store configuration. A submitted user identifier
444
+ does not select a tenant. A private purchase key is deliberately not a new
445
+ wire identifier: store-specific correlation remains an implementation duty.
446
+
447
+ | Record | Suggested key and contents | Invariant it protects |
448
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
449
+ | Purchase and subscription | Scoped store purchase key; verified evidence reference, current product, state, expiry, renewal intent, authoritative observation | Product changes do not change purchase identity; late observations do not blindly overwrite current state. |
450
+ | Account binding | Unique scoped purchase key; caller-owned user ID and retained occurrence | A purchase has at most one bound owner; retries cannot transfer it. |
451
+ | Store inbox | Scoped source observation identity; verified fact and processing status | Redelivery and no-op transitions do not create new lifecycle activity. |
452
+ | Event outbox | Unique event ID; serialized event bytes and routing scope | The state transition and its derived events commit together. |
453
+ | Delivery job | Event and destination; stable delivery-chain ID, attempts, next attempt, terminal status | A retry preserves event bytes and identity while refreshing the signature. |
454
+ | Consumer inbox | Trusted emitter context and signed body event ID; durable processing state | Duplicate delivery cannot duplicate an application effect. |
455
+
456
+ The model separates immutable observations from mutable current state. It
457
+ also separates a store purchase from an app account and a product: a user may
458
+ hold multiple purchases, a purchase may predate its binding, and a subscription
459
+ can change products. A table keyed only by `(userId, productId)` cannot retain
460
+ those distinctions.
461
+
462
+ Raw evidence belongs in restricted storage when the provider needs to retain
463
+ it. Tokenless account results are explicit projections of permitted fields,
464
+ validated before serialization. Copying a database record into a response and
465
+ then deleting known secrets is fragile because a later private field can escape
466
+ that deletion list.
467
+
468
+ ### 5.3 Purchase-to-access path
469
+
470
+ The following sequences follow one subscription purchase. The app team wires
471
+ the paywall callbacks and its authenticated backend API. Configuring a commerce
472
+ provider means giving that backend the provider's endpoint, supported app/store
473
+ configuration, and server credentials; it does not automatically install those
474
+ connections. A Paywall Service and Commerce Provider may be the same business
475
+ or separate businesses. Callback labels below are illustrative; client library
476
+ names differ by framework.
477
+
478
+ <!-- commerce-diagram: app-purchase -->
479
+
480
+ ```mermaid
481
+ sequenceDiagram
482
+ actor User
483
+ participant Paywall as Paywall Service
484
+ participant App as App code
485
+ participant SDK as OpenIAP SDK
486
+ participant Store as Apple / Google
487
+ Note over Paywall,App: App team wires purchase and result callbacks
488
+ User->>Paywall: Select a subscription
489
+ Paywall->>App: Product selection callback
490
+ App->>SDK: requestPurchase with store-fetched product / offer
491
+ SDK->>Store: Store purchase request
492
+ Store-->>SDK: Purchase result and evidence
493
+ SDK-->>App: Purchase callback
494
+ Note over App: Next: send evidence to the app backend
495
+ Note over App,Store: Pending, canceled or failed purchase: no new access
496
+ ```
497
+
498
+ **Figure 5a.** The paywall selects a product; the store processes the purchase.
499
+ A click or a client callback is not an entitlement grant. The app keeps its
500
+ existing pending, cancellation, and failure handling.
501
+
502
+ <!-- commerce-diagram: purchase-access -->
503
+
504
+ ```mermaid
505
+ sequenceDiagram
506
+ participant App as App code
507
+ participant Backend as App backend
508
+ participant Provider as Commerce Provider
509
+ participant Store as Apple / Google
510
+ Note over Backend,Provider: Configure provider URL and server credentials
511
+ App->>Backend: Evidence via authenticated API
512
+ Backend->>Backend: Authorize session user
513
+ Backend->>Provider: verifyPurchase(evidence)
514
+ Provider->>Store: Verify in app / store scope
515
+ Store-->>Provider: Store verdict
516
+ Provider-->>Backend: Require isValid: true
517
+ Backend->>Provider: bindPurchase(evidence, userId)
518
+ Provider-->>Backend: Require bound: true
519
+ Backend->>Provider: entitlements(userId)
520
+ Provider-->>Backend: Current productIds and records
521
+ Backend->>Backend: Durably record fulfillment
522
+ Backend-->>App: Fulfillment result and access
523
+ App->>App: finishTransaction via OpenIAP
524
+ App->>App: Report result to paywall
525
+ ```
526
+
527
+ **Figure 5b.** Verdict, ownership, and current access are separate decisions.
528
+ This is the successful path: rejected evidence, a failed binding, or an
529
+ operation error stops the attempt without a new grant. An empty entitlement
530
+ result grants no access. An operation outage is not a
531
+ revocation of previously established access; apply the existing access and
532
+ retry policy. The backend records the handling outcome before the app finishes
533
+ the store transaction, including the chosen store's acknowledgement or consumption
534
+ responsibility. These diagrams illustrate subscriptions, not consumable grants.
535
+ The app may also request account-free verification directly with its distinct
536
+ verification credential; that does not authorize the binding step.
537
+
538
+ Verification dispatches by the evidence's store and yields either a verdict
539
+ or an operation error. It does not touch account state. The authenticated
540
+ backend establishes that it may associate the purchase with its user; a token
541
+ and a user ID received from an app do not establish that authority by themselves.
542
+
543
+ Binding resolves evidence to a verified, scoped purchase and uses a unique
544
+ constraint or equivalent atomic comparison. If no owner exists it creates the
545
+ binding; if the same owner exists it returns success; otherwise it returns the
546
+ same `bound: false` result used for other non-binding outcomes. No API response
547
+ identifies the competing owner. Expired purchases can still have an owner;
548
+ the existence of the binding does not grant access.
549
+
550
+ At first binding, any entitlement event reflects the current gate, not the
551
+ history of unbound transitions. In the same transaction, retain an attributable
552
+ occurrence and enqueue a current grant only if access is still open. A grant
553
+ that expired before binding is not replayed. Where no attributable occurrence
554
+ was retained, the specification permits deferring the grant until the next
555
+ store observation; it does not permit inventing one.
556
+
557
+ Account reads enumerate the complete bounded record set, evaluate each gate
558
+ at provider read time, and form the aggregate answer. A second active purchase
559
+ can keep a product accessible when the first expires. An unknown record
560
+ contributes nothing; failure to classify or enumerate the required set causes
561
+ an operation error, not a partial success. A complete empty set is a valid
562
+ answer. The contract has no pagination mechanism in this version.
563
+
564
+ ### 5.4 Store transition and recovery path
565
+
566
+ <!-- commerce-diagram: lifecycle-delivery -->
567
+
568
+ ```mermaid
569
+ sequenceDiagram
570
+ participant Store as Apple / Google
571
+ participant Provider as Commerce Provider
572
+ participant Receiver as Event consumer
573
+ Note over Provider,Receiver: Register an app backend or external service receiver
574
+ Store->>Provider: Lifecycle notification
575
+ Provider->>Provider: Authenticate and reconcile
576
+ Provider->>Provider: Commit state and event outbox
577
+ Provider->>Receiver: Signed event over HTTPS
578
+ Receiver->>Receiver: Verify signature and schema
579
+ Receiver->>Receiver: Persist and deduplicate
580
+ Receiver-->>Provider: 2xx after durable acceptance
581
+ Note over Provider,Receiver: On retryable failure: same event/body, fresh signature
582
+ Receiver->>Receiver: Apply durable work idempotently
583
+ ```
584
+
585
+ **Figure 5c.** Lifecycle delivery is server to server. An event consumer registers
586
+ its receiver with the provider; it does not need to implement the provider APIs.
587
+ Receiving events does not grant server-role credentials. The app's authenticated
588
+ backend owns any current-access query.
589
+ A successful acknowledgement
590
+ ends retries for that delivery; exhausting the budget requires operational
591
+ recovery. No webhook goes to a shipped mobile app.
592
+
593
+ After authenticating a store observation and obtaining the authoritative facts
594
+ it needs, the provider reconciles against the latest stored purchase revision.
595
+ The mapping table selects the normalized event using store wire values,
596
+ subtypes, and history conditions. A receipt-created record is not evidence of
597
+ prior store-notification history. An informational or unmatched notification
598
+ does not justify an invented lifecycle event.
599
+
600
+ One transaction claims the observation, compares or locks the purchase
601
+ revision, derives the new state and its events, validates and serializes the
602
+ event bodies, and commits the state and delivery jobs together. A conflict
603
+ requires another reconciliation against current state. Store network calls
604
+ need not hold a database lock, but their results must not overwrite a newer
605
+ revision without that reconciliation. Store-specific corrections cannot be
606
+ reduced to “last arrival wins.”
607
+
608
+ An unchanged transition creates no event. A changed transition can create a
609
+ lifecycle event and, if bound and justified by a gate change, an entitlement
610
+ event. Event `active` uses derivation time, `processedAt`; the business
611
+ occurrence stays in `occurredAt`. The provider also arranges expiry handling
612
+ or evaluates access on reads so a known deadline takes effect without waiting
613
+ for another notification.
614
+
615
+ The delivery worker sends only committed event bytes. For each retry it
616
+ retains the body, event ID and delivery-chain ID, obtains the current signing
617
+ timestamp, and recalculates the signature. A finite retry budget ends in a
618
+ dead-letter record. Registration and connection-time destination checks keep
619
+ outbound delivery from becoming a path into private networks; redirects are
620
+ not followed. These operational obligations need implementation-owned tests,
621
+ because the portable event adapter does not test the full worker.
622
+
623
+ On the receiving side, select the emitter and valid secrets from trusted
624
+ endpoint configuration. Check the single timestamp and allowed skew, then
625
+ verify the signature over the raw bytes before interpreting the event. After
626
+ schema, version, and scope checks, take the event ID from the signed body and
627
+ commit a unique inbox record with durable work. Only then acknowledge. A crash
628
+ after acceptance can resume that work; a failure to persist remains retryable.
629
+
630
+ An inbox flag alone does not make an external effect atomic. Commit local
631
+ effects and completion together; for a remote effect use the destination's
632
+ idempotency mechanism or an outbox. A retried delivery can then repeat transport
633
+ without necessarily repeating the substantive effect. This is an implementation
634
+ strategy, not an exactly-once delivery claim.
635
+
636
+ ### 5.5 Acceptance and remaining decisions
637
+
638
+ The implementation is ready to declare a profile when its full obligations
639
+ are exercised, not when all routes return a JSON document. Begin with core and
640
+ one-store verification over REST. Add account lifecycle and entitlements as a
641
+ complete slice, then events. Add another binding over the same domain handlers
642
+ when needed, and run both together for parity.
643
+
644
+ | Injected condition | Result to demonstrate |
645
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
646
+ | Rejected evidence versus verifier outage | A negative verdict differs from `VERIFICATION_FAILED`; neither binds a user. |
647
+ | Two users race to bind one purchase | At most one owner; same-owner retries succeed without revealing the other identity. |
648
+ | Cancellation followed by known expiry | Access continues through the paid window and closes at the exclusive deadline. |
649
+ | Late, repeated, or equal-time events | No repeated effect or older overwrite; no invented ordering rule for equal timestamps. |
650
+ | Crash between state and delivery, or acceptance and effect | Committed work survives; no acknowledgement precedes durable acceptance. |
651
+ | Partial read, overflow, or unavailable authoritative state | An error remains an error; no fabricated complete entitlement result. |
652
+ | Erasure during queued delivery | Removed identity is not restored by concurrent work; downstream copies have an explicit erasure owner. |
653
+
654
+ Use an isolated instance with disposable users for conformance: operation
655
+ vectors exercise binding and erasure. Then use real store sandbox evidence for
656
+ verification and lifecycle tests. Keep the report's scope explicit; passing a
657
+ fixture verifier cannot establish that the real adapter verifies signatures,
658
+ selects the right environment, or maps store transitions correctly.
659
+
660
+ Some decisions intentionally stay outside the wire contract. Document store
661
+ setup and credential issuance, observation correlation, time-based freshness,
662
+ multi-purchase grant attribution, product replacement, bounded reads, retry
663
+ budgets, dead-letter recovery, and account recovery. During erasure coordinate
664
+ identities in records, queued work, and retained evidence with concurrent
665
+ workers. Do not rewrite an already delivered event under its old identity;
666
+ remove owned identity according to the erasure process and coordinate the
667
+ receiver's copies separately. During provider migration compare authoritative
668
+ reads and plan the overlap rather than assuming event IDs are shared.
669
+
670
+ ### 5.6 A recorded implementation and review process
671
+
672
+ The [implementation walkthrough](https://openiap.dev/commerce-protocol#build-walkthrough)
673
+ records six build milestones and a reviewed final source revision in a separate
674
+ [example project](https://github.com/hyodotdev/openiap-commerce-protocol-example),
675
+ adapted from an earlier internal prototype replaced by the example project. Each checkpoint includes the
676
+ AI task, code changes, an independently runnable source archive, checks, and a
677
+ capture of that version running:
678
+
679
+ | Milestone | Observable result |
680
+ | ------------------------ | -------------------------------------------------------------------------------- |
681
+ | Contract and persistence | A running HTTP server and an empty SQLite database |
682
+ | Verification | Accepted evidence creates an unbound purchase; account access remains empty |
683
+ | Binding | The server binds the purchase to Alice; a competing user cannot take it |
684
+ | Cancellation | Renewal stops while the remaining paid access stays open |
685
+ | Delivery | A failed receiver retries after restart; redelivery has one durable inbox effect |
686
+ | Expiry and recovery | Access closes at the exact deadline; reopening storage preserves state |
687
+
688
+ The [review log](https://openiap.dev/commerce-example/REVIEW.md) records an actual
689
+ schema failure and a response-viewer correction. The early backend returned
690
+ 500 because package 0.1.0 (protocol 1.0) requires a nonempty event-type list. Its temporary
691
+ `UNSUPPORTED_PROFILE` response also violated core discovery, as the external
692
+ review identified. These unfinished snapshots remain as history; discovery
693
+ works from checkpoint 4. The reviewed final revision adds the first-binding
694
+ grant event omitted by earlier versions. Long response lists were changed to
695
+ individual disclosures and checked on desktop and mobile. This records
696
+ implementation and review, not one-prompt generation. An [AI build brief](https://openiap.dev/commerce-example/build-brief.md)
697
+ sets the same milestones for an adopter's repository.
698
+
699
+ Install `openiap-commerce-protocol` with that repository's package manager. The
700
+ [final source checkpoint](https://openiap.dev/commerce-example/source.tar.gz)
701
+ installs the published contract package. The [archive verification report](https://openiap.dev/commerce-example/verification.json)
702
+ records extraction, source hashes, patch application from an empty directory,
703
+ and npm install/test results outside the OpenIAP workspace for every revision.
704
+ No IAPKit checkout is required.
705
+
706
+ This example uses a fictional store and a controlled clock. HTTP, SQLite,
707
+ signatures, and loopback delivery execute locally. It implements five REST
708
+ operations and a narrow lifecycle; it does not advertise protocol profiles or
709
+ bindings. Real store validation, login, erasure, GraphQL, public HTTPS delivery,
710
+ and production operations remain outside this example. Recovery reopens SQLite
711
+ within the same HTTP process; it does not test process-crash recovery.
712
+
713
+ The [checkpoint reports](https://openiap.dev/commerce-example/run.json) link the
714
+ checks, requests, and source hashes. A separate
715
+ [IAPKit comparison](https://openiap.dev/commerce-lab/run.json) runs IAPKit's
716
+ REST/GraphQL and commerce-helper tests with Convex and store I/O substituted,
717
+ and compares signatures with IAPKit's signer. These runs exercise local
718
+ behavior; they do not establish store validity or production conformance.
719
+
720
+ ### 5.7 An ecosystem of specialist and integrated products
721
+
722
+ A business need not own the whole purchase stack to serve apps that use OpenIAP.
723
+ A paywall specialist provides presentation and product selection. A commerce
724
+ provider owns verification, purchase ownership, and current access. An analytics
725
+ or automation service consumes normalized events. An integrated platform can
726
+ supply several of these roles. These are product roles, not new conformance
727
+ profiles or centrally registered classes of provider.
728
+
729
+ ```mermaid
730
+ flowchart TB
731
+ experience["Experience service<br/>paywalls, offers, experiments"]
732
+ app["App + OpenIAP client<br/>store products and purchase flow"]
733
+ store["App store<br/>purchase evidence"]
734
+ backend["Authenticated app backend<br/>session identity and fulfillment"]
735
+ commerce["Commerce service<br/>verification, ownership, access"]
736
+ data["Data and automation service<br/>analytics, attribution, CRM"]
737
+ experience -->|product selection| app
738
+ app <-->|store API| store
739
+ app <-->|evidence and access| backend
740
+ backend <-->|Commerce Protocol operations| commerce
741
+ commerce -->|signed events| data
742
+ commerce -.->|optional signed events| experience
743
+ ```
744
+
745
+ **Figure 6.** Business roles compose around the app. One platform may own several
746
+ boxes; the app can also connect specialists. OpenIAP distributes client libraries
747
+ and the commerce contract without operating a central commerce runtime.
748
+
749
+ | Role | Minimum integration deliverable | Evidence the adopter can run |
750
+ | ------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
751
+ | Experience | Product selection and purchase/result callbacks for the host app | Selected product reaches the OpenIAP purchase flow; pending, canceled, failed, and fulfilled outcomes are displayed |
752
+ | Commerce | Core discovery and complete advertised profiles/bindings; supported stores and server configuration | Profile checks, ownership isolation, time-based access, lifecycle and erasure checks; store sandbox evidence separately |
753
+ | Data and automation | Authenticated webhook endpoint, durable inbox, emitter/project scope | Signed delivery, retry, duplicate, malformed-input and optional-field handling |
754
+ | Integrated platform | The deliverables for each owned role with one owner per state transition | The same checks across its combined integration |
755
+
756
+ The paywall uses products fetched from the store and the app's existing purchase
757
+ callback; it cannot grant access from a selection or impression. Layouts,
758
+ targeting, product catalogs, and paywall UI APIs remain product-specific. A
759
+ consumer that only receives events implements receiver rules, not the `events`
760
+ emitter profile. Optional transaction and price fields remain unknown when
761
+ absent; lifecycle events alone do not supply a complete revenue ledger,
762
+ refund allocation, trial model, or attribution system.
763
+
764
+ The current OpenIAP client `verifyPurchaseWithProvider` helper supports IAPKit's
765
+ own API. Another provider connects through the app's authenticated backend,
766
+ which calls Commerce Protocol operations over REST or GraphQL. A provider name
767
+ or base URL change in that client helper is not a portable integration. The
768
+ example's `client-bridge.mjs` maps Apple/Google purchase fields into the installed
769
+ verification input schema on the backend; it does not validate store evidence.
770
+ Amazon and Horizon need explicit adapters for their store user identifiers.
771
+
772
+ Choose one authoritative ownership and entitlement service per app/project,
773
+ even when verification is delegated. Connected parties agree on opaque user
774
+ identity, issuer/project scope, credentials, stores, and versions. Compatibility
775
+ does not perform onboarding or ownership migration. The app still enforces
776
+ access and coordinates durable fulfillment and transaction finishing.
777
+
778
+ The [interactive role map](https://openiap.dev/commerce-protocol#architecture)
779
+ shows specialist and integrated arrangements. The [AI integration brief](https://openiap.dev/commerce-example/integration-brief.md)
780
+ starts from the role being delivered. The example provides executable request
781
+ mapping and a ready receiver, with a [signed HTTP ingestion report](https://openiap.dev/commerce-example/consumer-run.json).
782
+ These local fixture checks do not establish a real mobile checkout, a revenue
783
+ model, or full provider conformance.
784
+
785
+ ## 6. What this does not give you
786
+
787
+ The same person wrote the specification, the generators, the tests, the
788
+ reference implementation and this document. Consistency among them proves
789
+ they agree with each other, not that they are right. Independent review of
790
+ the store mappings and the expected outcomes is what would reduce that risk,
791
+ and it has not happened.
792
+
793
+ The checks are finite and selected. There is no proof over every possible
794
+ event sequence, no evidence from real store verification, and no
795
+ demonstration that two independently operated providers interoperate. The
796
+ [evaluation record](https://github.com/hyodotdev/openiap/blob/main/knowledge/research/commerce-protocol-evaluation.md)
797
+ states exactly what the current tests do and do not establish; the
798
+ [research agenda](https://github.com/hyodotdev/openiap/blob/main/knowledge/research/research-agenda.md)
799
+ states what would settle the open questions.
800
+
801
+ Some work stays with you, and one gap is worth naming precisely. For a read,
802
+ the contract does compose: SPEC §4.3 returns every product the user may access
803
+ right now, deduplicated, together with the subscription records whose open gates
804
+ produced them, so a second still-active record is accounted for. The gap is in
805
+ attribution and in the event model. The gate is per user and product, so an
806
+ entitlement event carries both, but a product change moves one subscription
807
+ from one product to another and the lifecycle vectors model that as a single
808
+ gate transitioning rather than the outgoing product's rights closing and the
809
+ incoming product's opening. Even when a refund identifies a transaction,
810
+ nothing says which grant loses authority while a later period or a second
811
+ purchase is still valid. Family sharing separates the purchaser from the
812
+ beneficiary, and a promotional grant has no purchase behind it at all; neither
813
+ appears in the contract. Account merging is declared out of scope. A single
814
+ subscription per user avoids only the part that comes from holding several at
815
+ once; a product change or a refunded earlier renewal still raises the question.
816
+ So the aggregate is given to you and the attribution is not: which grant a
817
+ right came from, and what a later correction does to it, are rules you write.
818
+
819
+ The contract also does not define a complete store transition machine,
820
+ universal purchase correlation, or historical-data migration. Where an expiry
821
+ or an event is missing you need an authoritative read; no envelope makes an
822
+ old observation current.
823
+
824
+ ## 7. When this is worth adopting
825
+
826
+ Start by reading Table 1 against the code you already have. That costs an
827
+ hour and needs no adoption at all. Find where your verification call fails
828
+ and confirm the failure does not become a rejection. Find your cancellation
829
+ handler and confirm it does not close access before the paid period ends.
830
+ Write one test that delivers a valid entitlement snapshot after its own
831
+ expiry and assert your gate stays closed. Passing all three does not mean
832
+ the integration is right; it means these three failures did not appear in
833
+ the cases you exercised. Account authority, duplicate effects and
834
+ portability are untested by them, and Table 1 is where to look next.
835
+
836
+ If one fails, the smallest useful step is still not adoption. For the
837
+ verification case it is SPEC §4.1's separation of a rejected verdict from an
838
+ operation error, so an unreachable verifier stops producing a negative
839
+ purchase verdict. For the other two it is the entitlement predicate in SPEC
840
+ §2.3, which SPEC §4.3 answers at a stated read time: evaluate access from
841
+ state and expiry, and stop reading a lifecycle label as an access decision.
842
+ Both are changes inside your own code.
843
+
844
+ Adopt the contract itself when you verify on a server for more than one
845
+ store, and want one decision to hold across stores whose evidence,
846
+ notifications and lifecycle vocabularies do not match, with the rules written
847
+ somewhere that does not belong to your provider. If you also ship several
848
+ client frameworks, the separate client contract in this repository addresses
849
+ that side; the two are complementary and neither requires the other.
850
+
851
+ The alternatives are worth naming honestly. A store's own server API is
852
+ authoritative but store-specific by construction, so using them directly
853
+ leaves the cross-store decision to you. A hosted entitlement service makes
854
+ that decision for you and documents it well. The mature ones publish their
855
+ cancellation, expiration and grace semantics, their duplicate handling, and
856
+ their versioning and deprecation commitments, and you can hold them to all
857
+ of it. What that documentation describes is how to use one service. Its
858
+ semantics are that service's own commitment rather than an obligation any
859
+ other provider has taken on. This contract is the third option: the same
860
+ decisions written as a specification any provider can implement, with one
861
+ set of checks that runs against all of them. It is younger and less proven
862
+ than either alternative, and it does not run anything for you. A relevant
863
+ analogue in shape is TM Forum's Product Inventory Management API
864
+ [[24]](#ref-24), a standard carrying a conformance profile and a test kit,
865
+ though what it standardizes is product inventory and its lifecycle
866
+ notifications for telecommunications rather than store purchase evidence and
867
+ the entitlement decisions that follow from it.
868
+
869
+ Do not adopt it expecting a hosted service; that is IAPKit's job, and IAPKit
870
+ is one implementation of this contract rather than the contract itself. Do
871
+ not adopt it expecting the checks to prove your provider correct. They test a
872
+ defined subset of obligations, which is a floor and not a guarantee.
873
+
874
+ The contract, the generated artifacts and the portable checks are published
875
+ under the same terms as the rest of the project, so a second implementation
876
+ can be built and held to them without asking anyone.
877
+
878
+ ## References
879
+
880
+ Scholarly metadata and reading status are maintained in
881
+ [bibliography.md](https://github.com/hyodotdev/openiap/blob/main/knowledge/research/bibliography.md);
882
+ the entries below reproduce what this document cites so that it can be read on
883
+ its own.
884
+
885
+ <!-- Scholarly entries are derived from bibliography.md; update metadata there first. -->
886
+
887
+ <a id="ref-1"></a>
888
+
889
+ [1] Collin Mulliner, William Robertson, and Engin Kirda. 2014.
890
+ _VirtualSwindle: An Automated Attack Against In-App Billing on Android._
891
+ ACM AsiaCCS. DOI: 10.1145/2590296.2590335.
892
+ [Publisher DOI](https://doi.org/10.1145/2590296.2590335).
893
+
894
+ <a id="ref-2"></a>
895
+
896
+ [2] Wenbo Yang, Yuanyuan Zhang, Juanru Li, Hui Liu, Qing Wang, Yueheng Zhang,
897
+ and Dawu Gu. 2017. _Show Me the Money! Finding Flawed Implementations of
898
+ Third-party In-app Payment in Android Apps._ NDSS.
899
+ [Paper](https://www.ndss-symposium.org/wp-content/uploads/2017/09/ndss2017_05A-2_Yang_paper.pdf).
900
+
901
+ <a id="ref-3"></a>
902
+
903
+ [3] Google. _Subscription lifecycle._ Google Play Billing documentation.
904
+ Vendor documentation; accessed 5 September 2026.
905
+ [Lifecycle guidance](https://developer.android.com/google/play/billing/lifecycle/subscriptions).
906
+
907
+ <a id="ref-4"></a>
908
+
909
+ [4] Apple. _status._ App Store Server Notifications documentation. Vendor
910
+ documentation; accessed 5 September 2026 through its Markdown
911
+ representation. [Status
912
+ definition](https://developer.apple.com/documentation/appstoreservernotifications/status).
913
+
914
+ <a id="ref-5"></a>
915
+
916
+ [5] OpenIAP contributors. _OpenIAP Commerce Protocol Specification 1.0._
917
+ Evaluated project artifact at commit
918
+ `100557091077dddcd8e1f22128f80bd2b9747912`, accessed 5 September 2026.
919
+ [Pinned specification](https://github.com/hyodotdev/openiap/blob/100557091077dddcd8e1f22128f80bd2b9747912/specs/commerce-protocol/SPEC.md).
920
+ This is the subject of the study, not independent evidence of its effectiveness.
921
+
922
+ <a id="ref-6"></a>
923
+
924
+ [6] Pat Helland. 2007. _Life beyond Distributed Transactions: an Apostate's
925
+ Opinion._ CIDR. Position paper.
926
+ [Paper](https://www.cidrdb.org/cidr2007/papers/cidr07p15.pdf).
927
+
928
+ <a id="ref-7"></a>
929
+
930
+ [7] CloudEvents authors. _CloudEvents, Version 1.0.2._ Technical specification;
931
+ accessed 5 September 2026.
932
+ [Pinned specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md).
933
+
934
+ <a id="ref-8"></a>
935
+
936
+ [8] Mark Utting, Alexander Pretschner, and Bruno Legeard. 2012.
937
+ _A Taxonomy of Model-Based Testing Approaches._ Software Testing,
938
+ Verification and Reliability 22(5), 297-312. DOI: 10.1002/stvr.456.
939
+ [University record](https://researchcommons.waikato.ac.nz/entities/publication/eb140299-43b5-4d35-8aae-5fcd8d519b90).
940
+
941
+ <a id="ref-9"></a>
942
+
943
+ [9] Jie Ma, Ningyu He, Jinwen Xi, Mingzhe Xing, Liangxin Liu, Jiushenzi Luo,
944
+ Xiaopeng Fu, Chiachih Wu, Haoyu Wang, Ying Gao, and Yinliang Yue. 2026.
945
+ _When Specifications Meet Reality: Uncovering API Inconsistencies in
946
+ Ethereum Infrastructure._ Proceedings of the ACM on Programming Languages
947
+ 10, OOPSLA1, Article 111. DOI: 10.1145/3798219.
948
+ [Open version](https://arxiv.org/abs/2603.06029).
949
+
950
+ <a id="ref-10"></a>
951
+
952
+ [10] Ding Yuan, Yu Luo, Xin Zhuang, Guilherme Renna Rodrigues, Xu Zhao, Yongle
953
+ Zhang, Pranay U. Jain, and Michael Stumm. 2014. _Simple Testing Can Prevent
954
+ Most Critical Failures: An Analysis of Production Failures in Distributed
955
+ Data-Intensive Systems._ OSDI, 249-265.
956
+ [Publisher record and paper](https://www.usenix.org/conference/osdi14/technical-sessions/presentation/yuan).
957
+
958
+ <a id="ref-11"></a>
959
+
960
+ [11] Daniel Reynaud, Eui Chul Richard Shin, Thomas R. Magrino, Edward X. Wu,
961
+ and Dawn Song. 2012. _FreeMarket: Shopping for free in Android applications._
962
+ NDSS.
963
+ [Programme entry](https://www.ndss-symposium.org/ndss2012/ndss-2012-programme/freemarket-shopping-free-android-applications/).
964
+
965
+ <a id="ref-12"></a>
966
+
967
+ [12] Rui Wang, Shuo Chen, XiaoFeng Wang, and Shaz Qadeer. 2011. _How to Shop
968
+ for Free Online: Security Analysis of Cashier-as-a-Service Based Web Stores._
969
+ IEEE Symposium on Security and Privacy, 465-480. DOI: 10.1109/SP.2011.26.
970
+ [Publisher DOI](https://doi.org/10.1109/SP.2011.26).
971
+
972
+ <a id="ref-13"></a>
973
+
974
+ [13] Shangcheng Shi, Xianbo Wang, and Wing Cheong Lau. 2021. _Breaking and
975
+ Fixing Third-Party Payment Service for Mobile Apps._ ACNS, LNCS 12727, 3-26.
976
+ DOI: 10.1007/978-3-030-78375-4_1.
977
+ [Publisher DOI](https://doi.org/10.1007/978-3-030-78375-4_1).
978
+
979
+ <a id="ref-14"></a>
980
+
981
+ [14] Pat Helland. 2012. _Idempotence Is Not a Medical Condition._ ACM Queue
982
+ 10(4), 30-46. DOI: 10.1145/2181796.2187821.
983
+ [Publisher DOI](https://doi.org/10.1145/2181796.2187821).
984
+
985
+ <a id="ref-15"></a>
986
+
987
+ [15] Vaggelis Atlidakis, Patrice Godefroid, and Marina Polishchuk. 2019.
988
+ _RESTler: Stateful REST API Fuzzing._ ICSE, 748-758.
989
+ DOI: 10.1109/ICSE.2019.00083.
990
+ [Publisher DOI](https://doi.org/10.1109/ICSE.2019.00083).
991
+
992
+ References 16 to 20 are the technical specifications the contract is built
993
+ on: its artifact formats, its requirement keywords, and the message
994
+ authentication its webhook signatures use. Reference 23 identifies a testing
995
+ tool the text compares against, and 24 a standard it is measured against.
996
+ None of them is evidence about OpenIAP.
997
+
998
+ <a id="ref-16"></a>
999
+
1000
+ [16] GraphQL Foundation. _GraphQL Specification, October 2021 Edition._
1001
+ Technical specification; accessed 6 September 2026. The repository does not
1002
+ pin an edition.
1003
+ [Specification](https://spec.graphql.org/October2021/).
1004
+
1005
+ <a id="ref-17"></a>
1006
+
1007
+ [17] JSON Schema. _JSON Schema Specification, draft 2020-12._ Technical
1008
+ specification; accessed 6 September 2026. The generated schemas declare this
1009
+ dialect.
1010
+ [Specification](https://json-schema.org/draft/2020-12).
1011
+
1012
+ <a id="ref-18"></a>
1013
+
1014
+ [18] OpenAPI Initiative. _OpenAPI Specification 3.1.0._ Technical
1015
+ specification; accessed 6 September 2026.
1016
+ [Specification](https://spec.openapis.org/oas/v3.1.0.html).
1017
+
1018
+ <a id="ref-19"></a>
1019
+
1020
+ [19] Scott Bradner. 1997. _Key words for use in RFCs to Indicate Requirement
1021
+ Levels._ RFC 2119. DOI: 10.17487/RFC2119.
1022
+ [RFC](https://www.rfc-editor.org/rfc/rfc2119).
1023
+
1024
+ <a id="ref-20"></a>
1025
+
1026
+ [20] Hugo Krawczyk, Mihir Bellare, and Ran Canetti. 1997. _HMAC: Keyed-Hashing
1027
+ for Message Authentication._ RFC 2104. DOI: 10.17487/RFC2104.
1028
+ [RFC](https://www.rfc-editor.org/rfc/rfc2104).
1029
+
1030
+ <a id="ref-21"></a>
1031
+
1032
+ [21] Steve Bishop, Matthew Fairbairn, Michael Norrish, Peter Sewell, Michael
1033
+ Smith, and Keith Wansbrough. 2005. _Rigorous specification and conformance
1034
+ testing techniques for network protocols, as applied to TCP, UDP, and
1035
+ Sockets._ SIGCOMM, 265-276. DOI: 10.1145/1080091.1080123.
1036
+ [Publisher DOI](https://doi.org/10.1145/1080091.1080123).
1037
+
1038
+ <a id="ref-22"></a>
1039
+
1040
+ [22] Yi Chen, Luyi Xing, Yue Qin, Xiaojing Liao, XiaoFeng Wang, Kai Chen, and
1041
+ Wei Zou. 2019. _Devils in the Guidance: Predicting Logic Vulnerabilities in
1042
+ Payment Syndication Services through Automated Documentation Analysis._
1043
+ USENIX Security, 747-764.
1044
+ [Publisher record and paper](https://www.usenix.org/conference/usenixsecurity19/presentation/chen-yi).
1045
+
1046
+ <a id="ref-23"></a>
1047
+
1048
+ [23] Pact Foundation. _How Pact works._ Tool documentation; accessed
1049
+ 6 September 2026.
1050
+ [Documentation](https://docs.pact.io/getting_started/how_pact_works).
1051
+
1052
+ <a id="ref-24"></a>
1053
+
1054
+ [24] TM Forum. _Product Inventory Management API (TMF637), v5.0._ Open API
1055
+ specification; accessed 6 September 2026.
1056
+ [Specification](https://www.tmforum.org/open-digital-architecture/open-apis/product-inventory-management-api-TMF637/v5.0).