@agent-cards/checkout 0.19.0 → 0.22.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 (106) hide show
  1. package/README.md +6 -753
  2. package/cdp.d.ts +1 -0
  3. package/cdp.js +2 -0
  4. package/index.d.ts +1 -0
  5. package/index.js +2 -0
  6. package/package.json +33 -33
  7. package/playwright.d.ts +1 -0
  8. package/playwright.js +2 -0
  9. package/preflight.d.ts +1 -0
  10. package/preflight.js +2 -0
  11. package/CHANGELOG.md +0 -132
  12. package/PREFLIGHT.md +0 -312
  13. package/dist/adyen-merchant-hosted.generated.d.ts +0 -277
  14. package/dist/adyen-merchant-hosted.generated.js +0 -1902
  15. package/dist/adyen.generated.d.ts +0 -24
  16. package/dist/adyen.generated.js +0 -64
  17. package/dist/attachment.d.ts +0 -11
  18. package/dist/attachment.js +0 -50
  19. package/dist/braintree.d.ts +0 -2
  20. package/dist/braintree.generated.d.ts +0 -10
  21. package/dist/braintree.generated.js +0 -302
  22. package/dist/braintree.js +0 -2
  23. package/dist/builtin-registry.generated.d.ts +0 -2
  24. package/dist/builtin-registry.generated.js +0 -1
  25. package/dist/card-fields.generated.d.ts +0 -3
  26. package/dist/card-fields.generated.js +0 -46
  27. package/dist/cdp.d.ts +0 -192
  28. package/dist/cdp.js +0 -2393
  29. package/dist/checkout-com.generated.d.ts +0 -4
  30. package/dist/checkout-com.generated.js +0 -183
  31. package/dist/client.d.ts +0 -849
  32. package/dist/client.js +0 -1754
  33. package/dist/cse-body.d.ts +0 -25
  34. package/dist/cse-body.js +0 -41
  35. package/dist/fiserv.d.ts +0 -65
  36. package/dist/fiserv.generated.d.ts +0 -73
  37. package/dist/fiserv.generated.js +0 -830
  38. package/dist/fiserv.js +0 -104
  39. package/dist/hosted-form.d.ts +0 -44
  40. package/dist/hosted-form.js +0 -78
  41. package/dist/index.d.ts +0 -18
  42. package/dist/index.js +0 -10
  43. package/dist/lifecycle.d.ts +0 -179
  44. package/dist/lifecycle.js +0 -395
  45. package/dist/mercado-checkout.d.ts +0 -20
  46. package/dist/mercado-checkout.generated.d.ts +0 -52
  47. package/dist/mercado-checkout.generated.js +0 -198
  48. package/dist/mercado-checkout.js +0 -108
  49. package/dist/merchant-handoff.d.ts +0 -54
  50. package/dist/merchant-handoff.js +0 -100
  51. package/dist/merchant-hosted.d.ts +0 -140
  52. package/dist/merchant-hosted.js +0 -170
  53. package/dist/merchant-total-watch.d.ts +0 -115
  54. package/dist/merchant-total-watch.js +0 -268
  55. package/dist/merchant-total.d.ts +0 -257
  56. package/dist/merchant-total.js +0 -383
  57. package/dist/owned-shop.generated.d.ts +0 -24
  58. package/dist/owned-shop.generated.js +0 -108
  59. package/dist/paysafe.generated.d.ts +0 -12
  60. package/dist/paysafe.generated.js +0 -87
  61. package/dist/playwright.d.ts +0 -3
  62. package/dist/playwright.js +0 -3
  63. package/dist/pre-claim.d.ts +0 -123
  64. package/dist/pre-claim.js +0 -386
  65. package/dist/preflight-capabilities.generated.d.ts +0 -1253
  66. package/dist/preflight-capabilities.generated.js +0 -1929
  67. package/dist/preflight-catalog.json +0 -4727
  68. package/dist/preflight-playwright.d.ts +0 -34
  69. package/dist/preflight-playwright.js +0 -355
  70. package/dist/preflight-schemas.json +0 -1122
  71. package/dist/preflight.d.ts +0 -1
  72. package/dist/preflight.generated.d.ts +0 -1965
  73. package/dist/preflight.generated.js +0 -570
  74. package/dist/preflight.js +0 -2
  75. package/dist/preparation.d.ts +0 -38
  76. package/dist/preparation.js +0 -191
  77. package/dist/prepared-processor.d.ts +0 -43
  78. package/dist/prepared-processor.js +0 -172
  79. package/dist/recurly.generated.d.ts +0 -1
  80. package/dist/recurly.generated.js +0 -87
  81. package/dist/registry.d.ts +0 -121
  82. package/dist/registry.js +0 -310
  83. package/dist/spreedly.generated.d.ts +0 -10
  84. package/dist/spreedly.generated.js +0 -332
  85. package/dist/stripe-checkout.d.ts +0 -81
  86. package/dist/stripe-checkout.generated.d.ts +0 -82
  87. package/dist/stripe-checkout.generated.js +0 -1093
  88. package/dist/stripe-checkout.js +0 -140
  89. package/dist/substitute.d.ts +0 -38
  90. package/dist/substitute.js +0 -23
  91. package/dist/substitutions.generated.d.ts +0 -11
  92. package/dist/substitutions.generated.js +0 -818
  93. package/examples/existing-browser.mjs +0 -63
  94. package/examples/preflight/classify-direct.mjs +0 -21
  95. package/examples/preflight/classify-kernel.mjs +0 -30
  96. package/examples/preflight/inspect-browser.mjs +0 -44
  97. package/examples/preflight/kernel-native/README.md +0 -112
  98. package/examples/preflight/kernel-native/documented-adapters.json +0 -113
  99. package/examples/preflight/kernel-native/inventory.json +0 -233
  100. package/examples/preflight/kernel-native/qualification.mjs +0 -182
  101. package/examples/preflight/kernel-profile.empty.json +0 -11
  102. package/examples/preflight/mollie-hosted.observations.json +0 -23
  103. package/examples/preflight/mollie-hosted.result.json +0 -103
  104. package/examples/preflight/stripe-script.direct.result.json +0 -92
  105. package/examples/preflight/stripe-script.observations.json +0 -16
  106. package/examples/preflight/stripe-script.result.json +0 -87
package/README.md CHANGED
@@ -1,759 +1,12 @@
1
1
  # @agent-cards/checkout
2
2
 
3
- Let your browser agents pay with **the user's own card**, without your
4
- infrastructure ever touching card data.
5
-
6
- Your agent drives checkout normally. When the page tries to tokenize a card, we
7
- pause that one request, ask the cardholder to approve on their device, and their
8
- device supplies the card and calls the merchant. You get back the response to
9
- replay. A real card never enters your process, your logs, or your network.
10
-
11
- ```
12
- your agent ──drives──> merchant checkout
13
- │ tokenization request
14
- ▼
15
- [ paused by this SDK ]
16
- │ template only, dummy card
17
- ▼
18
- Agentcard ──notify──> cardholder's device
19
- │ decrypts card locally
20
- ▼
21
- merchant's card vault
22
- ┌────────token────────┘
23
- ▼
24
- [ request resumed ] ──> order completes
25
- ```
26
-
27
- ## Install
28
-
29
- ```bash
30
- npm i @agent-cards/checkout
31
- ```
32
-
33
- Upgrading from 0.2.x? Read the [migration notes](./CHANGELOG.md), especially
34
- the unknown-outcome, cancellation and browser-context requirements.
35
-
36
- To inspect a checkout before card entry with SDK `0.9.0` or later, read [Check checkout support](./PREFLIGHT.md).
37
- The read-only helper detects all 23 registered PSPs and reports processor support separately
38
- from an identified checkout flow. Kernel native coverage requires its own capability profile.
39
-
40
- ## Use it
41
-
42
- Two lines against a CDP session you already have:
43
-
44
- ```ts
45
- import { VaultClient, attachToCdp } from '@agent-cards/checkout';
46
-
47
- const vault = new VaultClient({
48
- clientId: process.env.AGENTCARD_CLIENT_ID!,
49
- clientSecret: process.env.AGENTCARD_CLIENT_SECRET!,
50
- });
51
-
52
- // Pull the current processor list. attachToCdp arms the browser from it, so
53
- // without this you only intercept the processors built into your installed
54
- // version. Safe to call on every run: a failed fetch keeps the built-ins.
55
- await vault.syncRegistry();
56
-
57
- await attachToCdp(cdp, pageSessionId, {
58
- vault,
59
- user: 'usr_123', // whose card should pay
60
- merchant: 'vanman.shop',
61
- amount: 583, // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
62
- currency: 'usd', // "$5.83" is derived for the approval screen
63
- onApprovalUrl: (url) => sendToUser(url), // SMS, push, email, iMessage: your call
64
- });
65
- ```
66
-
67
- `amount` is your hint: an integer in the currency's smallest unit (583 for
68
- $5.83), or a decimal string in normal units ('5.83'), with `currency`. The
69
- processor's own amount is the higher authority: Agentcard reads it from the
70
- paused request where the processor puts it there, or from the Stripe intent
71
- the request names, right before the cardholder's device replays, and the
72
- company's caps are judged on it. A hint lets a bad purchase be refused the
73
- moment it opens; a hint more than one smallest unit away from the processor's
74
- amount is refused with nothing charged (`AmountMismatchError`), and after the
75
- replay the charge is reconciled against the approval
76
- (`ReplayResponse.amountVerified`, `chargedAmount`, and `chargedKind`:
77
- `captured` for a succeeded intent's `amount_received`, `authorized` for a
78
- manual-capture intent's `amount_capturable`, `none` when nothing is collected
79
- yet; plus the `checkout_authorization.amount_mismatch` webhook to your server
80
- when the charge disagreed). Every result carries `amountAuthority`:
81
- `processor`, `agent`, `page`, or `none`. A display string is never sent;
82
- Agentcard derives it.
83
-
84
- Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
85
- request for approval. Merchant timeouts still apply to the request itself: Square's observed tokenization deadline
86
- is about 10 seconds, Braintree's native request timeout is 60 seconds, and Adyen Web's own request timeout abandons its Sessions payment call 60 seconds after Pay (observed on Adyen Web 6.41 and 6.44), each including approval and handoff. These are the processors' limits, not ones the SDK enforces: the SDK's own authorization wait stays 15 minutes.
87
-
88
- The approval outlives the page's own request. When the page's script gives up on its paused card request while the cardholder is still deciding, the SDK keeps the approval pending (state `awaiting_approval`, reason `merchant_request_lost`, event `merchant_request_lost`) and answers the page's next request for the same purchase from it: same endpoint, same method, same top-level document, same page total, and a body of the same shape carrying the same card placeholder and the same amount when the request names one. An amount has to be in evidence somewhere (your `amount` hint, a `pageAmount` reader, or the request's own bytes); with none, a retry is a new question and the approval is retired unused. Braintree's client re-issues its mutation on its own within a second; on other processors the agent clicks Pay again. If the approval lands before the page asks again, the controller reads `ready_to_submit` with reason `awaiting_merchant_retry` (event `approval_awaiting_merchant_retry`): click Pay once, and that request is answered with no second prompt on the phone. The wait for that request is bounded (`merchantRetryWaitMs`, two minutes by default, never past the approval window); when it ends with no request, the SDK retires the approval through the API with reason `merchant_never_retried`, the controller reads `declined` with that reason, nothing was charged, and the cardholder's approval page says so. This applies to `token` and `cse` checkouts; a hosted form, a prepared checkout and a native Stripe Checkout step keep the behaviour below. For human approval on a short-deadline processor, `controller.prepare()` before the first Pay action, shown below, still finishes the request inside its own deadline. For Adyen, `prepare()` moves the approval before Pay, so only the device-side encryption runs inside Adyen Web's minute.
89
-
90
- Playwright:
91
-
92
- ```ts
93
- import { attachToPlaywright } from '@agent-cards/checkout/playwright';
94
- await attachToPlaywright(page, { vault, user, merchant, amount });
95
- ```
96
-
97
- ## Credentials
98
-
99
- Use your **OAuth client credentials**, not an `sk_` API key — those are retired,
100
- and the checkout endpoints reject them (`client_credentials_required`) because an
101
- authorization is bound to the confidential client that created it. The SDK does
102
- the `client_credentials` exchange for you, caches the token, and refreshes it
103
- once on a 401. Create a client from the dashboard Credentials page or with
104
- `agent-cards-admin oauth-clients create`.
105
-
106
- ## Why you need the SDK and not just `Fetch.enable`
107
-
108
- Card fields render in **cross-origin iframes**, which are separate CDP targets.
109
- Enabling `Fetch` on the page session never sees the tokenization request. You
110
- need recursive `Target.setAutoAttach({ flatten: true })` on every nested target,
111
- then `Fetch.enable` on each, then `Runtime.runIfWaitingForDebugger` to unpause
112
- them. That, plus which headers a merchant requires you to replay verbatim, is
113
- what this package encapsulates.
114
-
115
- ## What runs where
116
-
117
- | | Sees the real card |
118
- |---|---|
119
- | Your agent / browser | **no** — only a dummy PAN and a token |
120
- | Agentcard servers | **no** — a request template and a token |
121
- | Cardholder's device | yes — decrypts locally, calls the merchant directly |
122
-
123
- Because your process only ever handles a dummy card and an opaque token, this
124
- integration is designed to keep you out of PCI scope. Get your own QSA's read
125
- before you put that in writing.
126
-
127
- ## Supported processors
128
-
129
- Coverage is specific to the processor request format, merchant setup, browser transport and follow-up flow. A recognized endpoint is not proof that every store using that processor completes checkout.
130
-
131
- | Processor | Status |
132
- |---|---|
133
- | Shopify | supported, verified end to end |
134
- | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as `amount` + `currency` must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
135
- | Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK `0.5.0` confirmed merchant fulfillment and SDK `completed` using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. |
136
- | Checkout.com | supported |
137
- | Mercado Pago | Card tokenization and prepared checkout are implemented. Guest Checkout Pro in Mexico also corrects the issuer for one native card association when the selected card has the same brand and type. One live MXN 40 Lotería Chida purchase with published SDK 0.10.0 completed automatically through Pay; the merchant confirmed paid status and PDF fulfillment. The historical SDK result remains unknown because the private receipt adapter rejected a relative download URL; a separate read with the corrected adapter confirms that same paid receipt. Independent processor capture/settlement and other country or integration paths remain unverified. |
138
- | VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
139
- | Adyen | supported (mode `cse`): the vault encrypts the card for Adyen on the cardholder's device and your browser sends it. Sessions flow: the paused request is `/checkoutshopper/v1/sessions/{id}/payments` on Adyen's own hosts. A merchant that posts the encrypted fields to its own server pauses only when Agentcard reviewed it as a merchant profile (Dunelm and Cinemark so far); both profiles are in `observe`, so their card requests are reported and aborted and nothing pays there yet (see "Adyen merchants that take the card on their own server"). Without a preparation, approval starts at Pay and must land before Adyen Web's own request timeout (observed at 60 seconds on Adyen Web 6.41 and 6.44; not enforced by the SDK); `prepare({ psp: 'adyen' })` moves the approval before Pay |
140
- | Tranzila | supported (mode `hosted_form`): the cardholder finishes on Tranzila's own page; the paused form navigation resolves to a synthetic page, and you poll the merchant's order state |
141
- | Fiserv Commerce Hub | implemented (mode `cse`, prepared checkout only) and off until Agentcard turns it on for your company: the card frame's card capture on `connect.fiservapis.com` (or `connect-cert.fiservapis.com` in the sandbox), with the approved card encrypted on the cardholder's device under a key Agentcard reviewed for the merchant. A merchant whose frame posts to Fiserv's `payment-link` endpoint is not recognized. No live merchant purchase is verified yet |
142
-
143
- The recognizer list, and the status of each reviewed Adyen merchant profile on
144
- your client, are fetched from the API at runtime (`vault.syncRegistry()`),
145
- so new processors work without you shipping a release. `attachToCdp` derives the
146
- `Fetch.enable` url patterns from that same list rather than a constant, which is
147
- why the sync call belongs before the attach. `vault.cardUrlPatterns()` returns
148
- those patterns if you arm a CDP connection yourself. Call
149
- `GET /v2/checkout/recognizers?modes=token,cse,hosted_form&capabilities=fiserv_card_capture`
150
- for the list this build syncs. The API serves Fiserv's card capture only to a
151
- build that lists `fiserv_card_capture`, and only for a company Agentcard turned
152
- Fiserv on for. An older SDK never pauses a Fiserv request it could not finish,
153
- and no SDK pauses one for a company Fiserv is off for.
154
-
155
- ## Modes
156
-
157
- Each recognizer carries a `mode` (absent means `token`), and every authorization
158
- carries the mode it was handled in. The adapters do the right thing for all
159
- three; the difference matters if you drive `authorize()` yourself.
160
- `ReplayResponse` is a union, so branch on `mode`.
161
-
162
- - **`token`** (every processor but Adyen and Fiserv). The cardholder's device calls the
163
- processor and reports its answer; `authorize()` resolves with `status`,
164
- `headers` and `body` to fulfill the paused request with. The browser checks
165
- a fulfilled answer exactly as it checks a real one, so when the page called
166
- the processor cross-origin (Stripe always does: Checkout on
167
- `checkout.stripe.com` and Elements in the `js.stripe.com` frame both fetch
168
- `api.stripe.com`) the answer must carry `access-control-allow-origin` for
169
- the request's own `Origin`, or the page's fetch rejects and the checkout
170
- reports a connection error even though the cardholder approved. The
171
- adapters add those headers (`corsHeadersFor` + `withCorsHeaders`, exported
172
- for a runtime that fulfills by hand, and `corsDecision` when you also want
173
- the reason) and report the decision on the `authorized` event as `cors`:
174
- `echoed`, `same_origin`, or `none` (no usable Origin on the request, so
175
- the page could not read the answer). Only what a browser serializes is
176
- echoed: one canonical http(s) origin, or the opaque `null`. Shopify's
177
- card iframe posts to its own origin, so it never needed them.
178
- - **`cse`** (Adyen and Fiserv). Adyen's own page SDK encrypts the card before the request
179
- leaves the browser, so the paused body carries ciphertext. The cardholder's
180
- device produces the same ciphertext under the merchant's Adyen public key
181
- (fetched by Agentcard from Adyen's host when the request is parked) and
182
- `authorize()` resolves with `substitutions: { encoding: 'json', at, fields,
183
- remove }`. Write them into the paused body with
184
- `substituteEncryptedFields(body, substitutions)` and CONTINUE the request
185
- from the same browser (`Fetch.continueRequest` with the rewritten
186
- `postData`, or Playwright's `route.continue({ postData })`): its session
187
- data, risk data and cookies must stay its own. Only the four encrypted
188
- fields change, and the siblings named in `remove` are dropped (Adyen's
189
- `brand`, which adyen-web derived from the dummy digits the agent typed:
190
- left in place it names the wrong card and Adyen refuses the mismatch;
191
- absent, Adyen reads the brand off the card it decrypts). A body that lacks
192
- the fields throws `SubstitutionError`, which is not terminal. Adyen answers the browser,
193
- so `charged_kind` is null on the approval and the merchant's order state is
194
- the outcome to poll. A cse replay with `kind: 'merchant_hosted'` is an Adyen
195
- merchant's own endpoint instead: its `substitutions` name the profile, a JSON
196
- path (`[]` is the body root) and the merchant's field names, and
197
- `substituteMerchantHostedBody(body, replay)` writes it after checking the body
198
- against `bodySha256` (see "Adyen merchants that take the card on their own
199
- server"). A Fiserv card capture (a prepared checkout only) comes back with its
200
- envelope's four members at `source.encryptionData`: write them with
201
- `substituteFiservEnvelope(body, substitutions)`, never
202
- `substituteEncryptedFields`, and continue the request the same way.
203
- `substituteFiservEnvelope` throws, writing nothing, when the body is not the
204
- paused capture or the envelope names another key than the capture does. The
205
- API serves a Fiserv envelope only in the moments after the approval; a later
206
- read raises `PaymentOutcomeUnknownError` with reason `cse_substitutions_expired`.
207
- - **`hosted_form`** (Tranzila). The processor's hosted card form submits the
208
- card as a TOP-LEVEL form post, so the paused request is a page navigation
209
- (the adapters arm `Fetch.enable` with no resource-type filter and attach the
210
- processor's iframe, which is how a Document request on `direct.tranzila.com`
211
- gets paused at all). The cardholder's device rebuilds that form with the
212
- real card and submits it itself; the processor answers the device, and
213
- `authorize()` resolves with `{ mode: 'hosted_form', kind:
214
- 'submitted_on_device', outcome: 'unverified', submittedAt }` once the
215
- device reports the form left. **This is not an approved payment.** The
216
- stamp is the cardholder's device attesting that the form left it;
217
- Agentcard holds no processor evidence on this mode and cannot obtain any,
218
- so the API finishes the authorization as `submitted_on_device` (never
219
- `approved`) and sends your server `checkout_authorization.submitted`
220
- (never `.approved`). Treat it as "the person paid, or tried to, on their
221
- own device" and confirm the order with the merchant before you count it.
222
- There is no response to replay: FULFIL the paused navigation with
223
- `hostedFormSubmittedPage({ authorizationId, merchant, submittedAt })` (200,
224
- `text/html`, `x-agentcard-checkout: submitted_on_device`, a `<meta
225
- name="agentcard-checkout">` and an inert JSON block saying "submitted on
226
- the cardholder's device, payment unverified, do not resubmit"), the way
227
- the adapters do. Do not abort it: an aborted navigation renders nothing,
228
- the iframe silently keeps its dummy-card form, and the agent's next move is
229
- to click Pay again. Do not fake the processor's result page either: this
230
- SDK does not know the outcome. The adapters emit `submitted_on_device` (not
231
- `authorized`), refuse a byte-identical re-post of the same form for 15
232
- minutes (`hostedFormRepeatQuietMs`), and the API answers a regenerated one
233
- with `409 duplicate_submission`, which the adapters quiet the way they quiet
234
- a decline (`approvalCooldownMs`): the page's immediate re-posts are refused
235
- without a round trip, and once the prior authorization is declined or
236
- expired the same form is a new question. Confirm the order with the
237
- merchant, which learns the outcome from the processor.
238
-
239
- `syncRegistry()` asks the API for `SUPPORTED_MODES` only
240
- (`token,cse,hosted_form`), so a processor whose flow this build cannot finish
241
- is never paused; the API serves `hosted_form` entries only to callers that ask.
242
- A registry mode this SDK cannot finish throws `UnsupportedModeError` before
243
- creation. An approval returned in an unexpected mode has an unknown outcome
244
- and holds the attachment for reconciliation. The `authorized` event's detail names
245
- the `mode`, the `authorizationId` and, for `cse`, the `fields` that were
246
- substituted; it never carries ciphertext. The `submitted_on_device` event's
247
- detail names the `authorizationId`, `submittedAt` and `outcome:
248
- 'unverified'`; it is not an `authorized` event and must not be counted as
249
- one. `amountAuthority` on every replay is `stripe_payment_intent`,
250
- `hosted_form_sum` (the form's own amount) or `display_only`.
251
-
252
- ## Errors worth handling
253
-
254
- - `ApprovalTimeoutError` — the server confirms the authorization expired without a replay attempt. A local deadline is different: `PaymentOutcomeUnknownError` means the approval link may still be valid, so reconcile the merchant order before another attempt.
255
- - `ApprovalDeclinedError` — the user said no.
256
- - `AmountMismatchError`: the processor's amount did not match the amount the
257
- user was (or would have been) asked to approve. Nothing was charged. An
258
- `ApprovalDeclinedError` with `expectedCents`, `actualCents`, `currency`,
259
- `code: 'amount_mismatch'` and `stage`: `'pre_replay'` (checked right before
260
- the device would have sent the card; `authorizationId` names the declined
261
- authorization) or `'create'` (the intent already disagreed when the request
262
- was parked; no authorization exists, `authorizationId` is null). Per
263
- request, not per page: a merchant can still update an intent's amount
264
- until it is confirmed, so the adapters quiet the page's immediate retry
265
- and judge the next request afresh instead of latching.
266
- - `IntentNotConfirmableError`: the PaymentIntent was already charged, is
267
- processing, or is authorized and on hold (or canceled), so Agentcard
268
- refused to replay a confirm at it. An `ApprovalDeclinedError` with
269
- `code: 'intent_not_confirmable'`. Deliberately not "nothing was charged":
270
- check the intent at Stripe before retrying.
271
- - `ProcessorRefusedError`: the cardholder's device reported a processor
272
- request rejection. `pspErrorCode` carries the processor's code; optional
273
- `processorError` carries bounded Razorpay reason, source, step and payment/order
274
- identifiers when the API has them. A generic code such as `BAD_REQUEST_ERROR`
275
- does not establish an issuer decline or prove no money moved. Reconcile the
276
- merchant payment before retrying. This remains an `ApprovalDeclinedError`
277
- with `code: 'processor_refused'` for compatibility.
278
- The attachment records `status: 'declined', reason: 'processor_refused'`
279
- and holds further card requests. After confirming merchant failure, call
280
- `retryAfterMerchantFailure({ status: 'failed' })` to permit a deliberate new
281
- attempt immediately, without waiting for the user-decline cooldown. Do not
282
- automatically create a new attachment after this error;
283
- the guard applies only within the existing attachment.
284
- - `CheckoutApiError` with `code === 'amount_unverifiable'`: Stripe could not
285
- be asked (502; the SDK retries twice, 500ms then 1500ms, before throwing)
286
- or the paused request lacked its client secret or publishable key (400).
287
- `code === 'intent_not_confirmable'` at create (409) means the intent was
288
- already used; the adapters stop intercepting for that page.
289
- `code === 'cse_key_unavailable'` (502) means Adyen did not answer the
290
- public-key fetch and is retried the same way; `cse_client_key_unknown`
291
- (400) means Adyen does not know the merchant's `clientKey`, and
292
- `cse_template_unsupported` (400) means the paused body carries no
293
- encrypted card fields to fill (a stored card, a wallet, a single-blob
294
- `encryptedCard`); both are terminal for that page.
295
- `hosted_form_template_incomplete` (400) means the paused form lacks a
296
- field the device fills or the processor requires, `hosted_form_gated`
297
- (400) means it carries a live captcha token the device could never
298
- re-submit, and `hosted_form_field_refused` (400, with `field` and a
299
- `reason` of `stored_credential`, `not_a_sale` or `callback_host`) means
300
- the form asks the processor for something other than one plain sale
301
- reporting to the merchant you named (a reusable token in or out, a sale
302
- mode that is not a sale, a callback URL off the merchant's host: name the
303
- merchant by its hostname when the form carries callback URLs); all three
304
- are terminal for that page. `duplicate_submission` (409,
305
- with `prior_authorization_id` and `prior_status`) means this exact
306
- submission already has, or already had, its prompt: the adapters quiet the
307
- page's re-posts for `approvalCooldownMs` (no second notification for one
308
- payment) and judge the next request afresh, since the prior authorization
309
- declines or expires and the same form is then a new question.
310
- - `AdyenTestPlatformRefusedError`: a payment on Adyen's TEST platform was
311
- refused, since a test account can read whatever is encrypted under its key.
312
- Nothing was encrypted or charged. An `ApprovalDeclinedError` with `code`
313
- `adyen_test_environment_refused` (a live checkout names Adyen's test host or
314
- a `test_` client key; at `stage: 'create'` nothing was created and the
315
- adapters stop intercepting for that page) or
316
- `adyen_test_platform_requires_documented_test_card` (a test-mode checkout
317
- where the cardholder's card is not one of Adyen's documented test cards).
318
- The controller reads `failed` at `stage: 'create'` and `declined` at
319
- `stage: 'pre_replay'`, with the code as its reason, and the adapters' event
320
- names the code (`AdyenTestPlatformRefusedError: <code>`).
321
- A preparation refused for Adyen's test platform, or for a merchant profile
322
- that is not turned on, not offered to your account, or whose key or checkout
323
- page changed, throws `CheckoutPreparationError` with the API's code as its
324
- `reason`.
325
- - `CardEncryptedError`: this processor encrypts the card in-page and its
326
- registry entry does not (yet) say the vault can produce that ciphertext;
327
- route the purchase to an Agentcard-issued card instead.
328
- - `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
329
- before an authorization exists. Upgrade. An unexpected approved mode instead
330
- raises `PaymentOutcomeUnknownError` and requires reconciliation.
331
- - `PreparationRequiredError`: a direct `authorize()` call for a processor that
332
- pays only through a prepared checkout (Fiserv) passed no preparation. Nothing
333
- was created and nothing was charged; call `prepareCheckout()` first. The
334
- adapters refuse such a request before it pauses, with the controller reason
335
- `preparation_required`.
336
- - `SubstitutionError`: a `cse` approval could not be written into the paused
337
- body (the four encrypted fields were not there). The request is failed and
338
- the next one is judged afresh.
339
-
340
- ## Building this package
341
-
342
- It declares **no dependencies**, matching `packages/vault`, so the workspace
343
- lockfile needs no importer entry for it (a new package with its own deps cannot
344
- be installed here without regenerating the lockfile, and a full regen drifts
345
- unrelated transitive versions). Build it with the workspace TypeScript:
346
-
347
- ```bash
348
- cd packages/checkout && pnpm build && pnpm test
349
- ```
350
-
351
-
352
- ## Browser integration and merchant outcomes
353
-
354
- `attachToPlaywright` works with an existing Chromium page reached through
355
- `chromium.connectOverCDP`. Browserbase supplies `session.connectUrl`; Kernel
356
- supplies `browser.cdp_ws_url`; a custom browser must expose a compatible CDP
357
- endpoint. This is Agentcard's **direct SDK** path. Kernel's native Vault alias
358
- integration is a separate provider adapter with its own coverage and lifecycle;
359
- do not install both interceptors on the same checkout without validating how
360
- those routes interact.
361
-
362
- The local browser suite validates Chromium and nested cross-origin frames over
363
- both Playwright routing and a raw, session-aware CDP connection. It does not
364
- establish live Browserbase, Kernel, 3DS or merchant coverage. A raw page-scoped
365
- Playwright `CDPSession` is not the `CdpLike` interface. Raw CDP must preserve the
366
- `sessionId` on every command/event and allow recursive target attachment.
367
- Initial arming errors reject `attachToCdp`; a child that cannot be armed remains
368
- paused and reports `browser_interception_unavailable` for operator recovery.
369
-
370
- Await attachment before clicking Pay. Both adapters stop waiting for browser
371
- setup after 30 seconds and throw `CheckoutAttachmentError` with
372
- `code: 'checkout_attachment_failed'` and `reason: 'timeout'`, `'closed'` or
373
- `'unavailable'`. Use `attachmentTimeoutMs` to choose a setup deadline from 1 to
374
- 300000 milliseconds, separately from the approval's `timeoutMs`.
375
- Close the failed checkout page and create a fresh browser context before
376
- trying again. A late setup response cannot reopen the failed attachment or
377
- request approval; intercepted card requests remain blocked. A setup failure
378
- does not establish the status of any earlier purchase.
379
-
380
- Use a checkout context created with `serviceWorkers: 'block'`. Playwright cannot
381
- route requests intercepted by a service worker. The SDK rejects already active
382
- service workers, but that check cannot prevent a site from registering one
383
- later in an existing context configured to allow them. Attach before entering
384
- card fields; keep the existing checkout tab. Separate popup tabs need their own
385
- attachment. A page route does not cover a popup's first navigation; a popup
386
- which submits payment on that navigation requires a separately validated
387
- context/browser-level integration. Existing `page.route` handlers must call
388
- `route.fallback()` when they do not handle a request; later routes have priority.
389
-
390
- Both adapters now return a controller; existing code that ignores the return
391
- value continues to work. Choose `requireMerchantResult: true` for a pilot:
392
-
393
- ```ts
394
- const checkout = await attachToPlaywright(page, {
395
- vault, user, merchant, amount, currency,
396
- requireMerchantResult: true,
397
- onStateChange: state => recordState(state),
398
- onUserAction: action => deliverPrivatelyToUser(action),
399
- resolveMerchantResult: async state => readMerchantOrder(state),
400
- paymentEndpoints: [
401
- { origin: 'https://payments.example.com', pathname: '/submit', methods: ['POST'] },
402
- ],
403
- });
404
-
405
- // Your existing agent dispatches checkout. Later, once the paused request resumes:
406
- const state = await checkout.reconcile();
407
- if (state.status === 'completed') await finishAgentTask(state);
408
- ```
409
-
410
- `resolveMerchantResult` must verify the merchant's result for the original
411
- payment attempt. It returns one of:
412
-
413
- - `{ status: 'completed', orderId }`: merchant-confirmed success with a genuine order or receipt ID. Existing integrations keep this form.
414
- - `{ status: 'completed', confirmation: { kind: 'merchant_payment', authorizationId } }`: merchant-confirmed payment when no order or receipt ID is available. The ID must match the current checkout authorization.
415
- - `{ status: 'failed' }`: merchant confirmed the attempt failed; no successful payment/order exists.
416
- - `{ status: 'pending' }` or `{ status: 'unknown' }`: keep waiting or reconcile; never click Pay again.
417
- - `{ status: 'requires_user_action', reason: '3ds' | 'redirect' | 'other' }`: deliver your own browser live view or supported challenge UI to the user.
418
-
419
- For a merchant that confirms payment without returning an order ID, your
420
- resolver can return the explicit payment confirmation:
421
-
422
- ```ts
423
- const checkout = await attachToPlaywright(page, {
424
- vault, user, merchant, amount, currency,
425
- requireMerchantResult: true,
426
- resolveMerchantResult: async state => {
427
- const payment = await readOriginalMerchantPayment(state);
428
- if (!payment.confirmed || !state.authorizationId) return { status: 'unknown' };
429
- return {
430
- status: 'completed',
431
- confirmation: {
432
- kind: 'merchant_payment',
433
- authorizationId: state.authorizationId,
434
- },
435
- };
436
- },
437
- });
438
- ```
439
-
440
- `readOriginalMerchantPayment` is your merchant-specific check. The check must
441
- match the original payment request, amount, currency and selected card, and
442
- verify authoritative merchant success for that attempt. HTTP 200 alone, a card
443
- token, a success URL or text that anyone can open does not establish payment.
444
- Copying `state.authorizationId` without checking the payment is insufficient.
445
- The SDK checks the authorization binding; it does not independently authenticate
446
- the merchant evidence supplied by your resolver.
447
-
448
- Return one completion form at a time. The payment-confirmation form leaves
449
- `state.orderId` absent and exposes `state.confirmation` with the exported
450
- `MerchantPaymentConfirmation` type. Your consumer should finish on
451
- `state.status === 'completed'` and treat `orderId` as optional. A missing or
452
- mismatched authorization, or `{ status: 'completed' }` without either completion
453
- form, leaves the outcome unknown. Confirmed completion clears stale failure or
454
- authentication reasons and continues to block further payment submissions.
455
- Never invent an order ID from a token or authorization ID.
456
-
457
- The SDK does not infer order success from `authorized` or a tokenization reply,
458
- and does not claim to detect or solve arbitrary 3DS challenges. Your merchant
459
- resolver (or `checkout.requestUserAction('3ds')` when your browser observes it)
460
- drives that hook. Deliver `onUserAction` approval URLs privately: they are
461
- capabilities and never belong in general telemetry. Observer exceptions are
462
- isolated from the payment handoff.
463
-
464
- Native Stripe Checkout emits `checkout_blocked` before the existing `blocked`
465
- event when its local preparation rejects a request. Its
466
- `StripeCheckoutBlockedDetail` contains only fixed codes: `version: 1`,
467
- `processor: 'stripe'`, endpoint family, phase, stage, reason, optional validation code, gate state, and
468
- disposition. It contains no URLs, identifiers, request-derived field names or values, or exception text.
469
-
470
- | Field | Values |
471
- | --- | --- |
472
- | `endpoint_family` | `payment_methods`, `payment_page_confirm`, `other` |
473
- | `phase` | `tokenization`, `final`, `unknown` |
474
- | `stage` | `request_read`, `classification`, `claim`, `readiness`, `document`, `stub_response` |
475
- | `validation_code` | Shared-core `StripeCheckoutValidationCode`; present only for `request_validation_failed` |
476
- | `gate_state` | `fresh`, `stubbed`, `submitted`, `stopped` |
477
- | `disposition` | `active_claim_preserved`, `checkout_stopped` |
478
-
479
- `reason` is the exported `StripeCheckoutBlockReason` union. It identifies SDK
480
- claim and readiness failures, such as `duplicate_confirmation`, `document_changed`,
481
- or `attachment_not_ready`. A shared-core rejection reports
482
- `request_validation_failed` and a fixed `validation_code` identifying the failed
483
- check, or `unclassified` when no recognized code is available. Initial request
484
- classification uses phase `unknown`; a billing capture rejection uses
485
- `tokenization`, and a billing attachment rejection uses `final`.
486
- For example, `form_field_unknown` identifies an allowlist rejection without
487
- revealing the field name or value. Identifying that field requires a separate
488
- reviewed synthetic fixture; the code alone does not establish or fix a processor
489
- schema mismatch. `gate_state` records the state before the adapter handles
490
- the failure, though document validation may already have stopped the gate.
491
-
492
- `active_claim_preserved` means a valid duplicate was refused without invalidating
493
- the original request's claim. Do not cancel that checkout in response to the duplicate.
494
- `checkout_stopped` means the local preparation was retired; reconcile any existing
495
- authorization before another attempt. Neither disposition proves a payment outcome.
496
-
497
- With `requireMerchantResult`, subsequent card requests stay blocked after
498
- handoff. Stripe tokenization handoffs (`/v1/payment_methods`, `/v1/tokens`,
499
- `/v1/sources`, `/v1/confirmation_tokens`) always hold further recognized card
500
- requests, even when that option is false. A tokenization approval has no
501
- authoritative binding to a specific PaymentIntent, amount or currency, and the
502
- page cannot supply one. The SDK reports `awaiting_merchant` with reason
503
- `stripe_tokenization_unbound`, while ordinary browser traffic stays available.
504
-
505
- One follow-up can continue: the page's own card-free PaymentIntent confirm that
506
- pays with the approved token (`confirmCardPayment` with `payment_method: 'pm_...'`,
507
- `confirmPayment` with the confirmation token). The SDK asks the API, which reads
508
- the payment from Stripe and allows it only for the approved amount and
509
- currency, on the same Stripe account, paid with exactly the approved token.
510
- Allowed, the request continues untouched, the SDK emits `stripe_payment_continued`
511
- and the reason becomes `stripe_payment_continued`. One confirm continues per
512
- approval; every other follow-up, including an unrelated intent, a changed
513
- amount and a retry, stays blocked.
514
- Unrecognized merchant-server endpoints remain outside this guard unless listed
515
- in `paymentEndpoints`; this is not a guarantee against a merchant charging a
516
- saved token on its own server.
517
- A direct card-bearing PaymentIntent confirm remains supported with the backend's
518
- existing amount verification when `amount` and `currency` are supplied.
519
-
520
- Hosted-form submissions also always stay blocked because their payment outcome
521
- is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
522
- After an explicit merchant-confirmed failure, the application may call
523
- `checkout.retryAfterMerchantFailure({ status: 'failed' })` to permit another
524
- attempt where the attachment permits recovery. This is an assertion from your
525
- merchant integration, not a timeout or a best guess. Completed orders, cancelled
526
- attachments and attachments that issued an unbound Stripe token cannot reset
527
- this way, including when the token's browser delivery acknowledgement was lost.
528
- Reconcile the merchant outcome and use a separately validated checkout flow;
529
- do not reuse that token in a new attachment as a workaround. Configuration and
530
- unsupported-mode failures require fixing the integration. Bank flows requiring
531
- another confirmation and other stored-token chains remain unverified.
532
-
533
- Worldpay, Bambora and Mercado Pago preparation requires SDK 0.6.0 or later and the matching API and Vault release.
534
-
535
- Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before the first Pay action so the cardholder can approve before the native card request starts. The API and Vault deployments must support the selected processor:
536
-
537
- ```ts
538
- const checkout = await attachToPlaywright(page, {
539
- vault, user: 'your-user-id', merchant: 'Example merchant',
540
- amount: 100, currency: 'USD',
541
- onApprovalUrl: deliverPrivatelyToCardholder,
542
- });
543
- const preparation = await checkout.prepare({
544
- psp: 'braintree',
545
- environment: 'production', // Square, Braintree and Worldpay: production or sandbox
546
- });
547
- // The cardholder has consented and unlocked the same approval document.
548
- // No processor request or payment has started.
549
- await page.getByRole('button', { name: 'Pay', exact: true }).click();
550
- ```
551
-
552
- `prepare()` is available on both Playwright and raw CDP controllers. It requires `amount` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The approval page on the cardholder's device must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount's authority is `agent`; a card token does not enforce the merchant's eventual charge amount.
553
-
554
- | Processor | `environment` | Fresh native request |
555
- | --- | --- | --- |
556
- | Square | `production` or `sandbox` | Matching Square `/v2/card-nonce` host |
557
- | Braintree | `production` or `sandbox` | Matching Braintree GraphQL host and guest `TokenizeCreditCard` mutation |
558
- | Worldpay | `production` or `sandbox` | Matching Access Worldpay host and `/sessions/card` |
559
- | Checkout.com | `production` or `sandbox` | Fresh guest-card JSON POST to `/tokens` on the matching API or card-acquisition-gateway host |
560
- | Bambora | `shared` | `/scripts/tokenization/tokens` on `api.bam.shift4api.net` or `api.na.bambora.com` |
561
- | Mercado Pago | `shared` | `api.mercadopago.com/v1/card_tokens` with a fresh card body |
562
- | Recurly | `shared` | Form-encoded POST to `/js/v1/token` on `api.recurly.com` or `api.eu.recurly.com` |
563
- | Spreedly | `shared` | Native iframe JSON POST to `/v1/payment_methods/restricted.json` on `core.spreedly.com` |
564
- | Adyen | `production` or `sandbox` | The Sessions `/checkoutshopper/v1/sessions/{id}/payments` POST on the matching Adyen host family (`sandbox` is `checkoutshopper-test.adyen.com` with a `test_` client key; `production` is the live hosts with a `live_` key), carrying a fresh card's four encrypted fields |
565
- | Fiserv | `production` or `sandbox`, with `merchantProfile` | The card frame's `/ch/payments-vas/v1/card-capture` POST on `connect.fiservapis.com` (`production`) or `connect-cert.fiservapis.com` (`sandbox`), carrying a new card's envelope under the key `merchantProfile` names |
566
-
567
- Use `environment: 'shared'` for Bambora, Mercado Pago, Recurly and Spreedly because the same endpoint serves test and live requests. Agentcard cannot establish the processor's test mode from that URL or a credential prefix. Configure test mode through the merchant's processor account when testing. Agentcard's own `sandbox` flag remains separate. Prepared Worldpay, Bambora, Mercado Pago and Recurly requests reject saved-card and recurring request bodies; a refused request retires the local preparation. Reconcile any existing merchant attempt before creating a new attachment.
568
-
569
- Prepare a Checkout.com checkout before the agent clicks Pay:
570
-
571
- ```ts
572
- await controller.prepare({ psp: 'checkout_com', environment: 'production' });
573
- ```
574
-
575
- Keep the phone approval page open, then click Pay once after readiness. Checkout.com Flow 1.228.0 gives its card iframe 12 seconds to return a token; approval before Pay leaves that interval for request binding and tokenization. Prepared requests must use an exact `/tokens` URL on `api.checkout.com` or `card-acquisition-gateway.checkout.com`, or the corresponding `.sandbox.checkout.com` host for `sandbox`. The request must describe a fresh card with a CVV and public-key authentication. Wallet, saved-card, reusable-token and recurring variants are refused. A native API fallback after the first CAG request cannot reuse approval. Match the actual merchant charge to the approved amount separately; a token does not enforce that amount.
576
-
577
- Prepare an Adyen Sessions checkout before the agent clicks Pay:
578
-
579
- ```ts
580
- await controller.prepare({ psp: 'adyen', environment: 'production' }); // 'sandbox' for Adyen's test host
581
- ```
582
-
583
- After approval, click Pay once. adyen-web encrypts the agent's placeholder digits and posts its Sessions `/payments` request; the SDK pauses it and binds it to the approval, the cardholder's device (still on the approval page) encrypts the approved card under the merchant's Adyen key, and the request continues from your browser with only the four encrypted fields swapped and `brand` dropped. Adyen answers your browser, so the merchant's order state is the outcome to poll. The bound request must be a fresh `scheme` card on the approved host family with a client key of that environment; a stored-card, single-blob or store-the-card request never uses the approval. Adyen Web's own 60-second request timeout then covers only the pause, the bind and the device's encryption.
584
-
585
- Prepare a Fiserv checkout before the agent clicks Pay:
586
-
587
- ```ts
588
- // The attachment names the key's merchant: attachToCdp(cdp, sessionId, { ..., merchant: 'Agentcard sandbox' })
589
- await controller.prepare({ psp: 'fiserv', environment: 'sandbox', merchantProfile: 'agentcard_sandbox' });
590
- ```
591
-
592
- `merchantProfile` names the key Agentcard reviewed for the merchant, and the cardholder's approval page names that key's merchant. The checkout's `merchant` must be that merchant's name (`Agentcard sandbox` for `agentcard_sandbox`), so the approval, the preparation and the authorization name one merchant; any other name is refused with reason `merchant_profile_mismatch` before any network call. After approval, click Pay once. Fiserv's card frame encrypts the agent's placeholder and posts its card capture; the SDK pauses the capture and binds it to the approval, the cardholder's device encrypts the approved card under that key, and the capture continues from your browser with only the envelope's four members swapped. Fiserv answers your browser, so confirm the order with the merchant.
593
-
594
- A card capture with no preparation is refused before anyone is asked: the controller reads `failed` with reason `preparation_required`, nothing reaches Fiserv, and a later `prepare()` on that attachment is refused, so attach again. Fastlane, ACH, gift and EBT captures on the same endpoint continue untouched and leave the preparation for the card capture. Every other body on that endpoint counts as a card capture, including one the SDK cannot prove card-free (not JSON, over 64 KiB, or a plaintext card): with no preparation it is refused the same way, and with one the bind refuses it as `checkout_changed`. So does a capture carrying a header that neither Fiserv's card frame nor the browser writes, such as the `traceparent` header tracing tools add: it is not the request the cardholder approved. Every Fiserv payment needs the cardholder's approval: the envelope carries no expiry, so Autopilot never pays one. Fiserv is off for every company until Agentcard turns it on for yours; until then the API serves your SDK no Fiserv recognizer, so nothing pauses on a Fiserv checkout, and `prepare()` fails with reason `processor_unavailable`. A merchant whose key Agentcard has not turned on yet fails with `unsupported_checkout`.
595
-
596
- Prepare a Spreedly checkout before submitting the merchant's card form:
597
-
598
- ```ts
599
- await controller.prepare({ psp: 'spreedly', environment: 'shared' });
600
- ```
601
-
602
- After approval, submit the form once. The Vault replaces the card fields on your device and sends the native request through an encrypted connection to Spreedly. The merchant's environment and signed session stay unchanged. The API, Vault, relay and preparation migration must support Spreedly before you use this flow; the API refuses preparations while its relay rollout flag is disabled.
603
-
604
- Initial coverage includes the hosted iframe's new-card request. Saved-card CVV updates, Express, wallets, bank accounts and gateway purchase APIs require separate support. A returned card token does not confirm payment; wait for the merchant's result before reporting success. Autopilot is unavailable for Spreedly.
605
-
606
- Prepare Recurly before clicking the merchant's payment button:
3
+ The older name of [`@agent-cards/sdk`](../checkout/README.md), Agentcard's SDK. This package re-exports every entry of `@agent-cards/sdk` (`.`, `./cdp`, `./playwright`, `./preflight`) for one release, so an import of `@agent-cards/checkout` keeps working while you rename it. The two JSON files under `./preflight/` are not re-exported: read them from `@agent-cards/sdk/preflight/catalog.json` and `@agent-cards/sdk/preflight/schemas.json`.
607
4
 
608
5
  ```ts
609
- await controller.prepare({ psp: 'recurly', environment: 'shared' });
6
+ // before
7
+ import { VaultClient } from '@agent-cards/checkout';
8
+ // after
9
+ import { Agentcard } from '@agent-cards/sdk';
610
10
  ```
611
11
 
612
- After approval, submit the merchant's form once. Recurly's native request timer starts with that submission. The SDK preserves the captured US or EU endpoint and accepts a new-card form only. Legacy JSONP, saved tokens, bank accounts, alternative payments and proactive authentication requests require separate support. Prepared requests also refuse nonempty Worldpay or Cybersource risk results because those sessions can depend on the original card. A nonempty co-badged network preference is also refused until the selected card’s supported networks can be checked. A card token does not confirm a donation, subscription or purchase; confirm the merchant's result before reporting payment success.
613
-
614
- ### Use Checkout Pro in Mexico
615
-
616
- Attach the SDK before entering card fields on `www.mercadopago.com.mx`, then prepare the guest card checkout:
617
-
618
- ```ts
619
- await controller.prepare({ psp: 'mercado_pago', environment: 'shared' });
620
- ```
621
-
622
- The Vault checks the selected card's issuer using the merchant's current checkout configuration. Your browser receives the processor's original card-token response, and the SDK replaces the issuer in one native card association. The card number, security code and eight-digit prefix stay out of your SDK process. The encrypted relay carries the configuration lookup between the approval device and Mercado Pago.
623
-
624
- The selected card must have the same brand and card type as the native form. Missing or ambiguous configuration, changed checkout details, and a repeated card association stop continuation. Start a fresh checkout and approval after resolving the mismatch. Final Pay and merchant confirmation still follow your existing checkout integration.
625
-
626
- Use the matching SDK, API, Vault and relay releases together. The issuer correction covers the observed guest Checkout Pro flow in Mexico. CardForm or Bricks on merchant websites, other countries, installment changes and Autopilot need separate validation. Successful tokenization and issuer association do not establish a paid order; keep `requireMerchantResult` and a merchant receipt resolver when validating a purchase.
627
-
628
- Braintree's native `ClientConfiguration` GraphQL query can run before, during or after preparation without using the approval. Only a single `TokenizeCreditCard` mutation can consume the prepared Braintree checkout. Prepared requests require guest card tokenization with explicit `options.validate: false`; omitted validation options, saved-card fields and `validate: true` are refused. Other GraphQL operations, batches and compound mutations are blocked. Braintree's legacy REST fallback cannot consume a prepared authorization.
629
-
630
- Readiness lasts up to 30 seconds (`preparation.expiresAt`) and appears as `ready_to_submit`, with `paymentStatus: 'not_started'`. Trigger the caller-owned Pay action immediately after the promise resolves. Expiry, navigation, cancellation, an early request or a changed checkout fails closed. A preparation and its attachment are single use; reconcile any bound authorization before creating a new attachment. The SDK never clicks Pay, reuses a stale request, changes native request deadlines, or automatically retries a failed prepared checkout.
631
-
632
- After Pay, the processor's native deadline still covers fresh authorization binding, device replay and token handoff. A disconnected or backgrounded cardholder device, or a slow transport, can still miss it. A subsequent SCA challenge has its own lifetime after token handoff. If the page's own script abandons an unprepared `token` or `cse` request while the cardholder decides, the approval survives and answers the page's next request for the same purchase (see "The approval outlives the page's own request" above). If the frame that sent the request is removed, the document moves to another URL, the page closes or crashes, or a prepared request is abandoned, the attachment blocks further requests and tries to retire the pre-replay authorization; a started replay or unconfirmed cancellation remains unknown. Without `prepare()`, approval loading and human interaction still share the native deadline, so a delayed approval finishes the page's retry rather than its first request.
633
-
634
- Lost authorization polling, local approval timeouts, or interrupted browser
635
- handoffs produce `outcome_unknown` and block automatic retry. The thrown
636
- `PaymentOutcomeUnknownError` carries `authorizationId` when creation was
637
- acknowledged. `checkout.cancel()` stops the local attachment and polling; it
638
- does not revoke a pending approval link or undo a processor payment. Cancellation
639
- after an attempt starts is therefore unknown until reconciled. A cancelled
640
- attachment cannot restart.
641
-
642
- ### Adyen merchants that take the card on their own server
643
-
644
- Some Adyen merchants have adyen-web encrypt the card into four fields and post them to their own server, which then charges the card through Adyen. No processor host is involved, so no recognizer matches that request. Agentcard reviews each such merchant as a merchant profile: its endpoint, the body rules of its card request, and the Adyen key the card must be encrypted under. The SDK arms every profile this build reviewed from the start, in `observe`, so the agent's dummy card never reaches a reviewed merchant, even before `syncRegistry()` runs or when it fails. `syncRegistry()` asks the API for each profile's status on your client (`merchant_profiles=1`), and a profile turns `enabled` only when the API serves it enabled with the same rules this SDK build reviewed and this build ships it enabled. Both adapters pause the profile's endpoint and any sibling of it (the same path under another query) from attach, and judge each request there against the profile's status at that moment.
645
-
646
- - A request that carries no encrypted card (a basket query, a preflight) continues as before.
647
- - A body the browser did not hand over (a blob or a file, which neither adapter can read) is not proven card-free, so any request but a `GET`, `HEAD` or `OPTIONS` that carries one is treated as a card request the profile cannot finish. Playwright reports a missing body the same way, so an empty body is treated alike in both adapters.
648
- - A profile in `observe` never pays. Its card request is aborted before it reaches the merchant, and the adapters report `merchant_profile_observed` with the profile, the templated URL, the verdict and `keyPathSha256`, the SHA-256 of the body's sorted key paths (never a value; `null` for a body that was not read), then `unsupported_checkout`. The controller reads `unsupported` with reason `merchant_profile_observe`, and no authorization exists. Both reviewed profiles, `dunelm_was_graphql` and `cinemark_initiate`, ship in `observe`.
649
- - A card request the profile cannot finish (storing the card, a stored card, another operation, three fields instead of four, a sibling endpoint, an unreadable body) is aborted with `unsupported_checkout` and the reason, and reads `unsupported` with reason `merchant_request_unsupported`.
650
- - The profile's own card request, once Agentcard enables the profile for your client and this build, pays only through a preparation for that profile. Without one it is aborted before any pause, prompt or authorization: the adapters report `blocked` with `preparation_required`, the controller reads `failed` with that reason, and `prepare()` is refused on that attachment, as after any first card request. Create a new attachment and prepare before Pay.
651
-
652
- ```ts
653
- await controller.prepare({ psp: 'adyen', environment: 'production', merchantProfile: 'dunelm_was_graphql' });
654
- ```
655
-
656
- The environment is the profile's (`production` for a live Adyen key). After approval, click Pay once. The SDK sends the API the profile id with the paused request (`merchant_hosted: { profile }`, `content-type` as the only header, never Autopilot), and the cardholder's device encrypts the approved card under the key Agentcard reviewed for that merchant. The paused request's URL and body reach the API with that create, as every processor's do: the API checks the body against the profile and the amount, then keeps only its SHA-256 and the templated URL, so anything else in the body (a gift card number, a session token, an idempotency key) passes through Agentcard once and is not stored. The approval comes back as a `cse` replay with `kind: 'merchant_hosted'`, and the request continues from your browser with the four fields swapped and adyen-web's `brand` dropped, but only if the live body still hashes to the body the API checked at create (`bodySha256`). A continuation the SDK refuses sends nothing, and the SDK retires the approval (`merchant_never_retried`) so the API stops serving its ciphertext. The merchant's server charges the card and answers its own page; confirm the order with the merchant.
657
-
658
- After hand-off, a `5xx` final answer (after any redirect), a network failure, or the page closing before an answer arrived reports `payment_outcome_unknown` and holds the checkout (`outcome_unknown`). So does the page sending the card request again before an answer arrived. Every later card request to the same profile is aborted, since it carries the agent's dummy card and a prepared approval is single use. Events name a merchant profile's endpoint by its templated URL (origin, path template and the profile's own query), so a user id, cart id or email in the merchant's URL never reaches telemetry.
659
-
660
- #### Testing against your own Adyen TEST account
661
-
662
- A test-mode client can run the same checkout against its own server and its own Adyen TEST account, before any reviewed profile is enabled. Declare your endpoint on the client, before any attach, as taking the card request of a profile this build reviewed:
663
-
664
- ```ts
665
- const vault = new VaultClient({ clientId, clientSecret, sandboxMerchants: [
666
- { profile: 'dunelm_was_graphql', endpoint: 'https://pay.your-test-shop.example/graphql', clientKey: 'test_...' },
667
- ] });
668
- await controller.prepare({ psp: 'adyen', environment: 'sandbox', merchantProfile: 'dunelm_was_graphql' });
669
- ```
670
-
671
- The adapters then pause that exact endpoint (and its siblings) and judge its requests with the profile's body rules, whatever the profile's status. The preparation and the create carry `sandbox_declaration: { endpoint, client_key, environment: 'test' }`, and both name your endpoint's host as the merchant. The API accepts a declaration only from a test-mode client of an organization Agentcard has turned declarations on for. Your endpoint must be HTTPS on a public domain name, outside every payment processor's host, Adyen's and Agentcard's domains, and the domain of every merchant Agentcard reviewed (so `www.dunelm.com` is refused along with `was.dunelm.com`); the client checks the same rule when it is constructed. The API reads the key's public key from Adyen's TEST platform itself. The Vault page names your host, holds the bound request's key to the one the preparation read, and encrypts only Adyen's documented test cards under a `test_` key: a real card is refused on the device. A live client, or an organization without declarations turned on, gets `sandbox_declaration_refused`, and a declaration the API cannot pay gets `sandbox_declaration_invalid`, both as `CheckoutPreparationError` reasons at `prepare()`.
672
-
673
- On a client that declares a profile, that profile id names the declaration: `prepare({ merchantProfile })` with that id pays your endpoint, never the reviewed merchant. A test-mode client can never pay a reviewed merchant on Adyen's live platform anyway; use a separate live client for that.
674
-
675
- A raw CDP runtime arms `vault.merchantProfileUrlPatterns()` after `syncRegistry()`, beside `vault.cardUrlPatterns()`, reads `vault.merchantProfileOf(url)` and `classifyMerchantRequest(profile, url, method, body)` for each paused request, and writes a merchant-hosted approval with `substituteMerchantHostedBody(body, replay)`. Never continue a card body, or a body you could not read, to a profile's endpoint that `authorize()` did not pay.
676
-
677
- ### Unsupported endpoints
678
-
679
- `paymentEndpoints` is an explicit list supplied by the integrator after observing
680
- the site's payment requests. Each guard uses a canonical origin, exact path and
681
- mutation methods; it never examines or logs card bodies. If a guarded endpoint
682
- is not recognized, the SDK aborts it and reports `unsupported_checkout` /
683
- `unsupported` without creating an approval. Preflights and ordinary page traffic
684
- continue. There is no wildcard or intercept-all fallback, and no automatic
685
- conversion to an issued card. Unknown endpoints absent from these guards remain
686
- untouched; the SDK cannot identify every payment request from its URL.
687
-
688
- ### Runnable integration and local verification
689
-
690
- `examples/existing-browser.mjs` runs against an existing provider session, using
691
- an application-owned driver module for the agent's actions, user communication
692
- and merchant-result resolver. Set `CHECKOUT_DRIVER` to that module's absolute
693
- path and `CHECKOUT_CDP_URL` to the provider connection URL; optionally select the
694
- existing tab with `CHECKOUT_PAGE_INDEX`. The module must export
695
- `prepareCheckout(page)`, `submitCheckout(page)`, `resolveMerchantResult({page,
696
- state})`, `onUserAction(action, {page})`, and `finishAfterPayment({page, orderId})`.
697
- `prepareCheckout` returns the checkout options above. `submitCheckout` dispatches
698
- the existing agent's approved purchase and returns without waiting for approval.
699
- Install `playwright-core` in the example's host project. The SDK itself keeps no
700
- runtime dependencies. The example is integration scaffolding, not a universal
701
- merchant driver and not evidence of a live provider checkout.
702
-
703
- ```sh
704
- pnpm build
705
- pnpm test
706
- # Uses installed playwright-core, falling back to the monorepo backend dependency.
707
- # Set CHECKOUT_CHROME_PATH if Chromium is not installed in Playwright's cache.
708
- pnpm test:browser
709
- ```
710
-
711
- The browser fixtures never contact a payment service. The general suite uses
712
- `psp.invalid`; the Stripe continuation suite forces `api.stripe.com` through an
713
- allowlisted loopback proxy and a temporary self-signed TLS stub (requires the
714
- `openssl` CLI). All other proxy destinations are rejected. These suites use an
715
- in-process Agentcard API fixture and loopback merchant pages. The preparation
716
- fixture denies all external traffic and waits beyond each modeled request deadline before any card request: eleven seconds for Square, sixty-one seconds for Braintree, and six and a half seconds for Worldpay, Bambora and Mercado Pago. Each fixture then checks one fresh request with an unchanged abort timer. The five-second timer matches the inspected Worldpay and Bambora source; the Mercado Pago timer is a test boundary, not a measured native deadline. The fixture exercises SDK ordering with native-shaped request bodies and synthetic responses, not processor acceptance. It proves nested-frame pause/resume, agent control during approval,
717
- post-payment tasks in the same page, decline/expiry/cancel, unknown-outcome retry
718
- blocking, explicit unsupported endpoint behavior, and blocking an immediate real-browser
719
- Stripe token-to-intent fetch chain, including unrelated first intents, changed
720
- amounts/currencies and delayed CDP acknowledgement. It does not test card
721
- cryptography, real bank authorization or a cloud-provider deployment.
722
-
723
- Provider/API references checked for this integration:
724
- [Playwright CDP](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp),
725
- [Playwright routing limitations](https://playwright.dev/docs/api/class-page#page-route),
726
- [Browserbase Playwright quickstart](https://docs.browserbase.com/welcome/quickstarts/playwright),
727
- [Kernel native Agentcard integration](https://www.kernel.sh/docs/integrations/payments/agentcard).
728
-
729
-
730
- The authenticated `GET /v2/checkout/coverage` endpoint describes direct-SDK
731
- processor modes, limitations and verification levels. Use
732
- `POST /v2/checkout/coverage/assess` with up to 1,000 uniquely identified cases:
733
- `{ cases: [{ id, request_url, method: "POST", scenario: "one_time", weight: 1,
734
- requires_3ds: false }] }`. Scenarios also include `save_card`,
735
- `subscription_initial` and `subscription_renewal`. The endpoint assesses request
736
- recognition, not purchases: `recognized`, `unsupported` and `unverified` are
737
- coverage classifications, `recognized_traffic_share` is traffic-weighted, and
738
- `purchase_success_rate` stays null without observed merchant outcomes. Do not
739
- substitute the assessor for a browser/merchant validation run or use native
740
- Kernel adapter coverage as evidence for this SDK's coverage.
741
- ### Autopilot execution metadata
742
-
743
- When the cardholder has enabled an eligible spending rule in their vault, the same authorization can run through autopilot. The SDK keeps polling the existing authorization and returns optional `executionMode: 'autopilot' | 'user_approval'` and `grantId` metadata. Older API responses remain supported.
744
-
745
- `authorize()` accepts optional `executionMode` and `grantId` routing hints, sent as `execution_mode` and `grant_id`. These never establish permission to spend; the protected payment service checks the cardholder's signed rule. Autopilot suppresses `onApprovalUrl` while it is executing. A definite fallback to user approval delivers the existing authorization's URL once. A lost outcome remains `PaymentOutcomeUnknownError`; it does not create or submit a second payment.
746
-
747
- Use `executionMode: 'user_approval'` to require the existing confirmation flow. Without a selected card or grant, the backend can use exactly one eligible rule; ambiguous card selection keeps confirmation. A `grantId` restricts selection to that rule. The initial executor supports only the configured controlled Stripe test flow, and production remains disabled.
748
-
749
- For controlled hosted Stripe Checkout, both attachment functions accept `stripeCheckout: { sessionId, publishableKey, environment? }`. The default is TEST, requiring `cs_test_` and `pk_test_`. LIVE requires explicit `environment: 'production'`, a `cs_live_` Session and its `pk_live_` key. Both modes require `executionMode: 'autopilot'`, an explicit `grantId`, and a positive numeric USD `amount` in cents. The top-level document must be that exact Session on `https://checkout.stripe.com`, at `/c/pay/{sessionId}`, `/pay/{sessionId}`, `/g/pay/{sessionId}`, or `/f/pay/{sessionId}`. The SDK validates the existing URL without rewriting or navigating it. Direct `authorize()` calls use `stripeCheckoutEnvironment: 'production'` for the same explicit LIVE opt-in; the attachment functions pass it automatically.
750
-
751
- The SDK answers dummy-card tokenization locally, then sends the browser's final native confirmation through the shared core and enclave. The local response is preparation only: it creates no authorization and makes no processor request. The final result includes `checkoutSessionId` only after the selected grant, restricted terminal response, and captured amount have been checked. Continue to confirm the merchant order through the checkout controller.
752
-
753
- When Agentcard reports `autopilot_status: action_required` for the same authorization and grant, the SDK stops waiting and calls `onUserAction` with `reason: 'other'` and the authorization ID. The checkout controller records `requires_user_action` and holds further card requests. A direct `VaultClient.authorize()` call rejects with `PaymentOutcomeUnknownError` and reason `autopilot_action_required`. The existing authorization and reservation remain held; the SDK does not cancel or create another payment, return a processor payload, or infer a 3DS challenge. Your application's `resolveMerchantResult` can report the authoritative outcome of this same payment through `reconcile()`. Challenge execution and automatic continuation are not provided by this status report.
754
-
755
- The shared core carries a validated billing email from that tokenization request into the final confirmation before authorization. The email stays bound to the same Session and local PaymentMethod reference. Missing email stays missing; duplicate or conflicting values are refused. The SDK does not fill an email from the Agentcard account or invent one for the merchant.
756
-
757
- This integration requires matching backend and measured executor releases. TEST and LIVE have separate admission controls. LIVE also requires a newly authorized production grant on the selected real card, a fixed merchant account/profile with exactly one complete fixed-price USD line item and an independently reviewed operation qualification reference; the SDK option supplies none of these. LIVE deployment remains unqualified and disabled until that evidence exists. Exact observed totals do not establish atomic amount enforcement by Stripe. Subscriptions, saved-card flows and authentication continuations remain excluded. An unavailable preparation or uncertain result is declined or held for reconciliation; it cannot switch to a competing human-approval payment.
758
-
759
- The adapters also send the observed top-level HTTPS `merchantOrigin`, such as `https://shop.example`, without a path, query or trailing slash. Direct `authorize()` callers can supply that origin explicitly. It is a routing hint; the protected adapter independently verifies the processor account, payee and amount. If a browser cannot provide its top-level URL, an explicit `merchantOrigin` option can supply the hint; otherwise the regular approval path remains available.
12
+ `VaultClient` constructs the same client as `Agentcard` and stays for the same one release.