@agent-cards/checkout 0.19.0 → 0.21.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/README.md +6 -753
- package/cdp.d.ts +1 -0
- package/cdp.js +2 -0
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/package.json +33 -33
- package/playwright.d.ts +1 -0
- package/playwright.js +2 -0
- package/preflight.d.ts +1 -0
- package/preflight.js +2 -0
- package/CHANGELOG.md +0 -132
- package/PREFLIGHT.md +0 -312
- package/dist/adyen-merchant-hosted.generated.d.ts +0 -277
- package/dist/adyen-merchant-hosted.generated.js +0 -1902
- package/dist/adyen.generated.d.ts +0 -24
- package/dist/adyen.generated.js +0 -64
- package/dist/attachment.d.ts +0 -11
- package/dist/attachment.js +0 -50
- package/dist/braintree.d.ts +0 -2
- package/dist/braintree.generated.d.ts +0 -10
- package/dist/braintree.generated.js +0 -302
- package/dist/braintree.js +0 -2
- package/dist/builtin-registry.generated.d.ts +0 -2
- package/dist/builtin-registry.generated.js +0 -1
- package/dist/card-fields.generated.d.ts +0 -3
- package/dist/card-fields.generated.js +0 -46
- package/dist/cdp.d.ts +0 -192
- package/dist/cdp.js +0 -2393
- package/dist/checkout-com.generated.d.ts +0 -4
- package/dist/checkout-com.generated.js +0 -183
- package/dist/client.d.ts +0 -849
- package/dist/client.js +0 -1754
- package/dist/cse-body.d.ts +0 -25
- package/dist/cse-body.js +0 -41
- package/dist/fiserv.d.ts +0 -65
- package/dist/fiserv.generated.d.ts +0 -73
- package/dist/fiserv.generated.js +0 -830
- package/dist/fiserv.js +0 -104
- package/dist/hosted-form.d.ts +0 -44
- package/dist/hosted-form.js +0 -78
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -10
- package/dist/lifecycle.d.ts +0 -179
- package/dist/lifecycle.js +0 -395
- package/dist/mercado-checkout.d.ts +0 -20
- package/dist/mercado-checkout.generated.d.ts +0 -52
- package/dist/mercado-checkout.generated.js +0 -198
- package/dist/mercado-checkout.js +0 -108
- package/dist/merchant-handoff.d.ts +0 -54
- package/dist/merchant-handoff.js +0 -100
- package/dist/merchant-hosted.d.ts +0 -140
- package/dist/merchant-hosted.js +0 -170
- package/dist/merchant-total-watch.d.ts +0 -115
- package/dist/merchant-total-watch.js +0 -268
- package/dist/merchant-total.d.ts +0 -257
- package/dist/merchant-total.js +0 -383
- package/dist/owned-shop.generated.d.ts +0 -24
- package/dist/owned-shop.generated.js +0 -108
- package/dist/paysafe.generated.d.ts +0 -12
- package/dist/paysafe.generated.js +0 -87
- package/dist/playwright.d.ts +0 -3
- package/dist/playwright.js +0 -3
- package/dist/pre-claim.d.ts +0 -123
- package/dist/pre-claim.js +0 -386
- package/dist/preflight-capabilities.generated.d.ts +0 -1253
- package/dist/preflight-capabilities.generated.js +0 -1929
- package/dist/preflight-catalog.json +0 -4727
- package/dist/preflight-playwright.d.ts +0 -34
- package/dist/preflight-playwright.js +0 -355
- package/dist/preflight-schemas.json +0 -1122
- package/dist/preflight.d.ts +0 -1
- package/dist/preflight.generated.d.ts +0 -1965
- package/dist/preflight.generated.js +0 -570
- package/dist/preflight.js +0 -2
- package/dist/preparation.d.ts +0 -38
- package/dist/preparation.js +0 -191
- package/dist/prepared-processor.d.ts +0 -43
- package/dist/prepared-processor.js +0 -172
- package/dist/recurly.generated.d.ts +0 -1
- package/dist/recurly.generated.js +0 -87
- package/dist/registry.d.ts +0 -121
- package/dist/registry.js +0 -310
- package/dist/spreedly.generated.d.ts +0 -10
- package/dist/spreedly.generated.js +0 -332
- package/dist/stripe-checkout.d.ts +0 -81
- package/dist/stripe-checkout.generated.d.ts +0 -82
- package/dist/stripe-checkout.generated.js +0 -1093
- package/dist/stripe-checkout.js +0 -140
- package/dist/substitute.d.ts +0 -38
- package/dist/substitute.js +0 -23
- package/dist/substitutions.generated.d.ts +0 -11
- package/dist/substitutions.generated.js +0 -818
- package/examples/existing-browser.mjs +0 -63
- package/examples/preflight/classify-direct.mjs +0 -21
- package/examples/preflight/classify-kernel.mjs +0 -30
- package/examples/preflight/inspect-browser.mjs +0 -44
- package/examples/preflight/kernel-native/README.md +0 -112
- package/examples/preflight/kernel-native/documented-adapters.json +0 -113
- package/examples/preflight/kernel-native/inventory.json +0 -233
- package/examples/preflight/kernel-native/qualification.mjs +0 -182
- package/examples/preflight/kernel-profile.empty.json +0 -11
- package/examples/preflight/mollie-hosted.observations.json +0 -23
- package/examples/preflight/mollie-hosted.result.json +0 -103
- package/examples/preflight/stripe-script.direct.result.json +0 -92
- package/examples/preflight/stripe-script.observations.json +0 -16
- 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
|
-
|
|
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
|
-
|
|
6
|
+
// before
|
|
7
|
+
import { VaultClient } from '@agent-cards/checkout';
|
|
8
|
+
// after
|
|
9
|
+
import { Agentcard } from '@agent-cards/sdk';
|
|
610
10
|
```
|
|
611
11
|
|
|
612
|
-
|
|
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.
|