@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.
- package/CONVENTION.md +168 -0
- package/DESIGN.md +1056 -0
- package/README.md +227 -5
- package/SPEC.md +1471 -0
- package/conformance/index.d.ts +303 -0
- package/conformance/index.mjs +2126 -0
- package/conformance/mock-provider.mjs +491 -0
- package/examples/entitlement-granted-no-subscription.json +12 -0
- package/examples/entitlement-revoked.json +21 -0
- package/examples/provider-capabilities.json +209 -0
- package/examples/store-event-mapping.json +287 -0
- package/examples/subscription-canceled.json +22 -0
- package/examples/subscription-product-changed.json +30 -0
- package/examples/subscription-renewed.json +29 -0
- package/examples/verify-purchase-request.json +6 -0
- package/examples/verify-purchase-result.json +7 -0
- package/generated/bindings/graphql-operations.json +87 -0
- package/generated/bindings/http-binding.json +143 -0
- package/generated/bindings/introspection-signature.json +320 -0
- package/generated/bindings/operations-sdl.json +4 -0
- package/generated/bindings/operations.graphql +366 -0
- package/generated/commerce-protocol.graphql +1219 -0
- package/generated/openapi/commerce-protocol.openapi.json +1413 -0
- package/generated/schemas/commerce-event.schema.json +499 -0
- package/generated/schemas/commerce-protocol.bundle.schema.json +1576 -0
- package/generated/schemas/operations.schema.json +578 -0
- package/generated/schemas/primitives.schema.json +101 -0
- package/generated/schemas/provider-capabilities.schema.json +205 -0
- package/generated/schemas/store-event-mapping.schema.json +211 -0
- package/generated/vectors/lifecycle.json +908 -0
- package/generated/vectors/operations.json +1122 -0
- package/package.json +62 -12
- package/schema/01-primitives.graphql +102 -0
- package/schema/02-commerce-event.graphql +195 -0
- package/schema/03-provider-capabilities.graphql +139 -0
- package/schema/04-store-event-mapping.graphql +98 -0
- package/schema/05-operations.graphql +461 -0
- package/schema/06-compiler-vocabulary.graphql +139 -0
- package/schema/07-protocol-metadata.graphql +76 -0
- package/src/index.d.ts +63 -0
- package/src/index.mjs +121 -0
- 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).
|