@agent-cards/sdk 0.21.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 (103) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/PREFLIGHT.md +30 -0
  3. package/README.md +1070 -21
  4. package/dist/aci.d.ts +173 -0
  5. package/dist/aci.generated.d.ts +87 -0
  6. package/dist/aci.generated.js +306 -0
  7. package/dist/aci.js +371 -0
  8. package/dist/adyen-merchant-hosted.generated.d.ts +3 -3
  9. package/dist/adyen-merchant-hosted.generated.js +283 -49
  10. package/dist/agentcard.d.ts +9 -2
  11. package/dist/agentcard.js +4 -2
  12. package/dist/amadeus-checkout.generated.d.ts +57 -0
  13. package/dist/amadeus-checkout.generated.js +549 -0
  14. package/dist/amadeus.d.ts +41 -0
  15. package/dist/amadeus.js +81 -0
  16. package/dist/builtin-registry.generated.js +1 -1
  17. package/dist/card-endpoint.d.ts +177 -0
  18. package/dist/card-endpoint.js +286 -0
  19. package/dist/card-fields.generated.d.ts +5 -3
  20. package/dist/card-fields.generated.js +284 -7
  21. package/dist/cdp.d.ts +5 -1
  22. package/dist/cdp.js +841 -135
  23. package/dist/client.d.ts +403 -19
  24. package/dist/client.js +949 -98
  25. package/dist/creditguard.generated.d.ts +79 -0
  26. package/dist/creditguard.generated.js +662 -0
  27. package/dist/cse-body.js +11 -1
  28. package/dist/datatrans.d.ts +96 -0
  29. package/dist/datatrans.generated.d.ts +64 -0
  30. package/dist/datatrans.generated.js +426 -0
  31. package/dist/datatrans.js +178 -0
  32. package/dist/elavon-converge.d.ts +37 -0
  33. package/dist/elavon-converge.generated.d.ts +49 -0
  34. package/dist/elavon-converge.generated.js +439 -0
  35. package/dist/elavon-converge.js +84 -0
  36. package/dist/eprotect.d.ts +152 -0
  37. package/dist/eprotect.generated.d.ts +77 -0
  38. package/dist/eprotect.generated.js +498 -0
  39. package/dist/eprotect.js +222 -0
  40. package/dist/hosted-form.d.ts +3 -2
  41. package/dist/hosted-form.js +3 -2
  42. package/dist/index.d.ts +17 -3
  43. package/dist/index.js +6 -1
  44. package/dist/juspay-gate.d.ts +82 -0
  45. package/dist/juspay-gate.js +126 -0
  46. package/dist/juspay.generated.d.ts +89 -0
  47. package/dist/juspay.generated.js +705 -0
  48. package/dist/lifecycle.d.ts +57 -6
  49. package/dist/lifecycle.js +119 -8
  50. package/dist/merchant-card-endpoints.generated.d.ts +63 -0
  51. package/dist/merchant-card-endpoints.generated.js +1103 -0
  52. package/dist/merchant-hosted.d.ts +4 -1
  53. package/dist/merchant-hosted.js +5 -2
  54. package/dist/merchant-session-probe.d.ts +48 -0
  55. package/dist/merchant-session-probe.js +53 -0
  56. package/dist/merchant-session.generated.d.ts +69 -0
  57. package/dist/merchant-session.generated.js +919 -0
  58. package/dist/payrails.d.ts +133 -0
  59. package/dist/payrails.generated.d.ts +73 -0
  60. package/dist/payrails.generated.js +596 -0
  61. package/dist/payrails.js +305 -0
  62. package/dist/pelecard.d.ts +32 -0
  63. package/dist/pelecard.generated.d.ts +32 -0
  64. package/dist/pelecard.generated.js +283 -0
  65. package/dist/pelecard.js +42 -0
  66. package/dist/pre-claim.d.ts +72 -2
  67. package/dist/pre-claim.js +857 -5
  68. package/dist/preflight-catalog.json +2218 -287
  69. package/dist/preflight-schemas.json +179 -6
  70. package/dist/preflight.generated.d.ts +1 -1
  71. package/dist/preflight.generated.js +233 -4
  72. package/dist/preparation.d.ts +5 -2
  73. package/dist/preparation.js +36 -10
  74. package/dist/prepared-processor.d.ts +84 -5
  75. package/dist/prepared-processor.js +147 -6
  76. package/dist/primer.d.ts +53 -0
  77. package/dist/primer.generated.d.ts +38 -0
  78. package/dist/primer.generated.js +624 -0
  79. package/dist/primer.js +82 -0
  80. package/dist/registry.d.ts +100 -8
  81. package/dist/registry.js +85 -11
  82. package/dist/shiji-sts.d.ts +144 -0
  83. package/dist/shiji-sts.generated.d.ts +72 -0
  84. package/dist/shiji-sts.generated.js +913 -0
  85. package/dist/shiji-sts.js +222 -0
  86. package/dist/stripe-checkout.generated.d.ts +1 -1
  87. package/dist/stripe-checkout.generated.js +699 -9
  88. package/dist/stripe-setup.d.ts +16 -0
  89. package/dist/stripe-setup.js +61 -0
  90. package/dist/substitutions.generated.js +187 -29
  91. package/dist/verifone.d.ts +68 -0
  92. package/dist/verifone.generated.d.ts +51 -0
  93. package/dist/verifone.generated.js +1401 -0
  94. package/dist/verifone.js +119 -0
  95. package/dist/voice-types.d.ts +27 -0
  96. package/dist/voice.d.ts +6 -1
  97. package/dist/voice.js +6 -1
  98. package/dist/wspay.d.ts +41 -0
  99. package/dist/wspay.generated.d.ts +41 -0
  100. package/dist/wspay.generated.js +370 -0
  101. package/dist/wspay.js +65 -0
  102. package/examples/preflight/kernel-native/inventory.json +1 -1
  103. package/package.json +3 -3
package/README.md CHANGED
@@ -160,7 +160,21 @@ Playwright: `agentcard.checkout.attachToPlaywright(page, { user, merchant, amoun
160
160
 
161
161
  `amount` is your hint. The processor's own amount is the higher authority: Agentcard reads it from the paused request or from the Stripe intent the request names, right before the cardholder's device replays, and the company's caps are judged on it. A hint more than one smallest unit away from the processor's amount is refused with nothing charged (`AmountMismatchError`), and after the replay the charge is reconciled against the approval (`ReplayResponse.amountVerified`, `chargedAmount`, `chargedKind`, plus the `checkout_authorization.amount_mismatch` webhook when the charge disagreed). Every result carries `amountAuthority`: `processor`, `agent`, `page`, or `none`.
162
162
 
163
- Merchant timeouts still apply to the request itself: Square's observed tokenization deadline 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. These are the processors' limits; the SDK's own authorization wait stays 15 minutes. 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`) and answers the page's next request for the same purchase from it; when the approval lands first, the controller reads `ready_to_submit` with reason `awaiting_merchant_retry`, and one more Pay click completes the purchase. The wait for that request is bounded (`merchantRetryWaitMs`, two minutes by default); an approval the page never asks for again is retired as `merchant_never_retried`. For a short-deadline processor, `controller.prepare()` before the first Pay finishes the request inside its own deadline.
163
+ An `amount` in neither form, such as the string `"2667"`, is refused before anything is created, and the error names what you passed and each amount it can mean, in the forms the API accepts:
164
+
165
+ ```text
166
+ amount is an integer in the currency's smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06"). You passed the string "2667". For 26.67 USD, send the number 2667 or the string "26.67". For 2667.00 USD, send the number 266700 or the string "2667.00".
167
+ ```
168
+
169
+ Attach with the amount as a number in the smallest unit, and the SDK accepts it:
170
+
171
+ ```ts
172
+ await agentcard.checkout.attachToCdp(cdp, pageSessionId, { user: 'usr_123', merchant: 'vanman.shop', amount: 2667, currency: 'usd' });
173
+ ```
174
+
175
+ `prepare()` refuses the same amount with `CheckoutPreparationError` reason `amount_required`, and its message carries the same sentences. A value below zero is named as such and never offered back as a positive amount.
176
+
177
+ Merchant timeouts still apply to the request itself: Square's observed tokenization deadline 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. These are the processors' limits; the SDK's own authorization wait stays 15 minutes. 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`) and answers the page's next request for the same purchase from it; when the approval lands first, the controller reads `ready_to_submit` with reason `awaiting_merchant_retry`, and one more Pay click completes the purchase. The wait for that request is bounded (`merchantRetryWaitMs`, two minutes by default); an approval the page never asks for again is retired as `merchant_never_retried`. For a short-deadline processor, `controller.prepare()` before the first Pay finishes the request inside its own deadline. A Shiji STS getTokenId is the exception: its approval answers only the request it was created from, and the SDK cancels it as `merchant_request_aborted` when the page abandons that request.
164
178
 
165
179
  ### Pause only, and let your server create
166
180
 
@@ -218,6 +232,12 @@ what this package encapsulates.
218
232
  | Agentcard servers | **no**, a request template and a token |
219
233
  | Cardholder's device | yes, decrypts locally, calls the merchant directly |
220
234
 
235
+ On a reviewed merchant's own card endpoint (mode `card_endpoint`, below) the
236
+ device sends the card to the merchant through Agentcard's checkout relay, which
237
+ carries end-to-end TLS it cannot read. Agentcard's servers hold the paused
238
+ request's session and the merchant's answer encrypted, for minutes, and hand
239
+ each over once.
240
+
221
241
  Because your process only ever handles a dummy card and an opaque token, this
222
242
  integration is designed to keep you out of PCI scope. Get your own QSA's read
223
243
  before you put that in writing.
@@ -235,11 +255,26 @@ Coverage is specific to the processor request format, merchant setup, browser tr
235
255
  | 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. |
236
256
  | 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 |
237
257
  | 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 |
258
+ | ACI Worldwide COPYandPAY (Halfords) | dark launch: served only after `syncRegistry()` and only for an org Agentcard enabled; prepared checkout only. The card-number request is replayed from the cardholder's device, the page's own CVC and expiry requests are answered locally, and `/payment` continues once. See [Pay an ACI COPYandPAY checkout](#pay-an-aci-copyandpay-checkout) |
238
259
  | 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 |
239
260
  | 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 |
261
+ | Worldline MyCheckout | implemented (mode `hosted_form`) and off until Agentcard turns it on for your company: the card form on Worldline's hosted payment page, a top-level form POST to `/checkout` on `payment.pay1` or `payment.pay2` under `checkout.worldline-solutions.com` (pre-production: `payment.pay1.preprod` and `payment.pay2.preprod`). The cardholder's device submits Worldline's form with the approved card; the paused navigation resolves to the synthetic page, and you poll the merchant's order state. The form names no amount, so pass `amount` and `currency`, above zero: that is what the cardholder approves. A card submit with a checked "remember" box, a Google Pay token or a Click to Pay answer is refused. Google Pay and Click to Pay payments that carry no card number continue untouched, as the page's cancel and its other payment methods do: no authorization, no prompt. A MyCheckout page on a merchant's own subdomain is not recognized. No live merchant purchase is verified yet |
262
+ | Verifone eCommerce (verifone.js) | implemented (mode `cse`, prepared checkout only) and off until Agentcard turns it on for your company: the SDK pauses a request at a merchant whose page encrypts the card with verifone.js and posts it to the merchant's own server only when Agentcard reviewed that merchant as a Verifone profile. The Container Store is the one profile, in `observe`, and Agentcard has reviewed neither its card request nor its key yet, so the SDK reports and aborts a request there that carries a verifone.js blob, and nothing pays (see "Verifone merchants that encrypt the card in the page"). Agentcard has not verified a live merchant purchase yet |
240
263
  | Gravy (Gr4vy) Secure Fields | not supported: recognized, not intercepted, and the preflight reports `unsupported` with `request_method_not_supported`. The card request is Secure Fields' update of a checkout session: a JSON body to `/checkout/sessions/<session id>/fields` on the merchant's own host `api.<merchant>.gr4vy.app` (sandbox: `api.sandbox.<merchant>.gr4vy.app`), carrying `payment_method.number`, `payment_method.expiration_date` as one `MM/YY` value and `payment_method.security_code`. Gravy publishes this request as an HTTP PUT and the SDK pauses and replays POST requests only, so today the SDK recognizes the destination but the request continues to Gravy untouched. The shape follows Gravy's published Secure Fields contract and is not confirmed by a capture from a live merchant form. Updating the session stores the card with Gravy; the merchant creates the transaction from the session on its own server, so a completed request is not a payment. Gravy Embed, the hosted frame on `embed.<merchant>.gr4vy.app`, is not covered |
241
264
  | GMO Payment Gateway | dark launch (mode `token`): only for an org Agentcard has enabled; see [Pay a GMO Payment Gateway checkout](#pay-a-gmo-payment-gateway-checkout) |
242
265
  | CyberSource Flex Microform | dark launch: Microform 2.12.1 card fields only, after `vault.syncRegistry()`, and only for an org Agentcard has enabled. For any other org the API does not serve the recognizer, so nothing pauses there. The cardholder's device seals the card for CyberSource and the card field receives the transient token. Validated in local Chromium; no live merchant purchase yet. See [Pay a CyberSource Flex Microform checkout](#pay-a-cybersource-flex-microform-checkout) |
266
+ | Juspay Express Checkout | dark launch: the new-card flow Swiggy's website sends from its own page, for an org Agentcard has enabled, after `vault.syncRegistry()`. Validated in local Chromium with no live merchant purchase yet; see [Pay a Juspay checkout](#pay-a-juspay-checkout) |
267
+ | Reviewed merchant card endpoints: AT&T, Italo, Universal Orlando, Target Optical, Opus Virtual Offices | recognized in mode `card_endpoint`, every one in observe: the adapters pause each merchant's own card request and refuse it as `unsupported_checkout` before any authorization, so the dummy card never reaches the merchant. A profile takes payments only after a reviewed partner HAR, a reviewed release of this SDK and the API, and per-org enablement |
268
+ | Datatrans Secure Fields (PCI Proxy; British Airways) | dark launch: the card-number iframe's TOKENIZE card post only, after `vault.syncRegistry()`, and only for an org Agentcard has enabled (see [Pay a Datatrans Secure Fields checkout](#pay-a-datatrans-secure-fields-checkout)). The cardholder's device sends the card to Datatrans and the iframe receives the token and its `cardInfo`. A post that asks PCI Proxy to keep the card on file, which British Airways's Nexus checkout sends for every new card, is refused before any prompt while Agentcard's card-on-file policy refuses it, so a Nexus checkout does not complete yet |
269
+ | Authorize.Net Direct Post Method and SIM | dark launch (mode `hosted_form`, capability `authorize_net_dpm_form`): served only to an org Agentcard enabled for it. The card form posts one form navigation to `transact.dll` on `secure2`, `secure` or `test.authorize.net`; the cardholder's device submits it, and you poll the merchant's order state. Not eligible for auto-approval |
270
+ | Pelecard (Iframe/Redirect 2.0) | dark: paused only for an org Agentcard enables for Pelecard, in test mode only. One-time card payments on `gateway21` and `gateway20.pelecard.biz`; pass `amount`, `currency` and the page's total as `pageAmount`. See [Pay a Pelecard checkout](#pay-a-pelecard-checkout) |
271
+ | CreditGuard (Hyp PPS hosted payment page) | dark launch, prepared checkout only (see "Pay a CreditGuard checkout"): served to this build (`capabilities=creditguard_pps_process`) only for a company Agentcard enabled for it, and prepared or paid only for that company. The cardholder's device sends the card and the page reads CreditGuard's answer. 3-D Secure, OTP, installments, save-my-card and currency conversion are refused. No UAT or live capture yet |
272
+ | Primer (Web SDK v2 hosted fields; Printful, GetYourGuide) | dark launch (mode `token`): only for an org Agentcard has enabled; see [Pay a Primer checkout](#pay-a-primer-checkout) |
273
+ | Payrails web SDK v6 (Blacklane) | dark launch (mode `device_encrypt`): only for an org Agentcard has enabled, and only the card form's single-use save until card-on-file consent exists; see [Pay a Payrails checkout](#pay-a-payrails-checkout) |
274
+ | Elavon Converge (Hosted Payment Page) | dark: paused only for an org Agentcard enables for Converge, always with the cardholder's approval, never autopilot. One credit card sale in US dollars per payment page on `api.convergepay.com` (`api.demo.convergepay.com` in Converge's demo environment); no amount needed. See [Pay an Elavon Converge payment page](#pay-an-elavon-converge-payment-page) |
275
+ | WSPay Components (WSPay's build of Monri Components) | dark launch: the card frame's confirm of a new card for a one-time payment only, after `vault.syncRegistry()`, with `amount` and `currency`, and only for an org Agentcard has enabled; for any other org the SDK leaves WSPay's requests alone. The cardholder's device sends the card to WSPay and the frame receives WSPay's answer. A confirm that asks WSPay to keep the card on file is refused, and Valamar asks on every booking. Validated in local Chromium against a model of the card frame; no WSPay sandbox run or live merchant purchase yet. See [Pay a WSPay Components checkout](#pay-a-wspay-components-checkout) |
276
+ | Shiji STS (MyCheck Wallet V3; La Quinta by Wyndham) | dark launch, mode `cse` under a pinned key, and only for an org Agentcard has enabled. Every card request is refused in the browser today (`shiji_sts_keep_on_file`): the card token a hotel booking mints guarantees the stay, and Agentcard cannot yet ask the cardholder to let the hotel keep the card. See [Shiji STS](#shiji-sts-mycheck-wallet-v3) |
277
+ | Amadeus Checkout (Air France, KLM) | dark: only for an org Agentcard enabled, and only through `prepare({ psp: 'amadeus_checkout' })`. The Checkout Web SDK's card add pauses; every other action on its URL continues. Air France's and KLM's add is refused as `amadeus_merchant_held_expiry` and every Data Collection Page store as `amadeus_dcp_field_config_unreviewed`, so neither merchant completes yet. See "Prepare an Amadeus Checkout card step" |
243
278
 
244
279
  The recognizer list, and the status of each reviewed Adyen merchant profile on
245
280
  your client, are fetched from the API at runtime (`agentcard.checkout.syncRegistry()`),
@@ -247,17 +282,29 @@ so new processors work without you shipping a release. `attachToCdp` derives the
247
282
  `Fetch.enable` url patterns from that same list rather than a constant, which is
248
283
  why the sync call belongs before the attach. `agentcard.checkout.cardUrlPatterns()` returns
249
284
  those patterns if you arm a CDP connection yourself. Call
250
- `GET /v2/checkout/recognizers?modes=token,cse,hosted_form&capabilities=fiserv_card_capture`
285
+ `GET /v2/checkout/recognizers?modes=token,cse,hosted_form,device_encrypt&capabilities=fiserv_card_capture,gmo_shared_pk,capture_context,juspay_txns.v1,datatrans_secure_fields_tokenize,authorize_net_dpm_form,pelecard_transaction,aci_split_card.v1,creditguard_pps_process,primer_payment_instruments,payrails_device_encrypt,converge_hpp_process,worldpay_eprotect_jsonp,wspay_confirm,shiji_sts_pinned_key,amadeus_checkout_actions.v1,worldline_mycheckout_form,verifone_card_capture`
251
286
  for the list this build syncs. The API serves Fiserv's card capture only to a
252
287
  build that lists `fiserv_card_capture`, and only for a company Agentcard turned
253
288
  Fiserv on for. An older SDK never pauses a Fiserv request it could not finish,
254
- and no SDK pauses one for a company Fiserv is off for.
289
+ and no SDK pauses one for a company Fiserv is off for. The API serves Juspay's
290
+ card flow the same way, behind `juspay_txns.v1`, Datatrans's Secure Fields
291
+ card post behind `datatrans_secure_fields_tokenize`, the Authorize.Net DPM
292
+ or SIM form behind `authorize_net_dpm_form`, Pelecard's card submission
293
+ behind `pelecard_transaction`, ACI's split card behind `aci_split_card.v1`,
294
+ CreditGuard's hosted page submit behind `creditguard_pps_process`,
295
+ Primer's card tokenization behind `primer_payment_instruments`, WSPay's card
296
+ confirm behind `wspay_confirm`, Shiji STS's card leg behind
297
+ `shiji_sts_pinned_key`, Amadeus Checkout's card add behind
298
+ `amadeus_checkout_actions.v1`, Worldline MyCheckout's card form behind
299
+ `worldline_mycheckout_form`, and Payrails' card save behind
300
+ `payrails_device_encrypt` to a build that also asks for `device_encrypt`. The
301
+ same holds for the Verifone profiles and `verifone_card_capture`.
255
302
 
256
303
  ## Modes
257
304
 
258
305
  Each recognizer carries a `mode` (absent means `token`), and every authorization
259
- carries the mode it was handled in. The adapters do the right thing for all
260
- three; the difference matters if you drive `authorize()` yourself.
306
+ carries the mode it was handled in. The adapters do the right thing for each
307
+ mode; the difference matters if you drive `authorize()` yourself.
261
308
  `ReplayResponse` is a union, so branch on `mode`.
262
309
 
263
310
  - **`token`** (every processor but Adyen and Fiserv). The cardholder's device calls the
@@ -305,8 +352,9 @@ three; the difference matters if you drive `authorize()` yourself.
305
352
  paused capture or the envelope names another key than the capture does. The
306
353
  API serves a Fiserv envelope only in the moments after the approval; a later
307
354
  read raises `PaymentOutcomeUnknownError` with reason `cse_substitutions_expired`.
308
- - **`hosted_form`** (Tranzila). The processor's hosted card form submits the
309
- card as a TOP-LEVEL form post, so the paused request is a page navigation
355
+ - **`hosted_form`** (Tranzila, Authorize.Net DPM and SIM). The processor's
356
+ hosted card form, or the merchant's own card form that posts to the
357
+ processor, submits the card as a form post, so the paused request is a navigation
310
358
  (the adapters arm `Fetch.enable` with no resource-type filter and attach the
311
359
  processor's iframe, which is how a Document request on `direct.tranzila.com`
312
360
  gets paused at all). The cardholder's device rebuilds that form with the
@@ -336,15 +384,98 @@ three; the difference matters if you drive `authorize()` yourself.
336
384
  without a round trip, and once the prior authorization is declined or
337
385
  expired the same form is a new question. Confirm the order with the
338
386
  merchant, which learns the outcome from the processor.
387
+ - **`device_encrypt`** (Payrails). The page encrypts the card under a
388
+ per-session key that the paused request does not carry: it came in the
389
+ Payrails init payload the merchant's backend handed the page. The adapters
390
+ read that payload from the page's own XHR and fetch answers and send its key
391
+ with the create as `request.payrails.encryption_public_key`; a caller that
392
+ runs its own interception passes it the same way, and a Payrails save
393
+ without it throws before anything is created. The cardholder's device
394
+ encrypts the real card under that key and sends the save to Payrails
395
+ itself, and `authorize()` resolves with `{ mode: 'device_encrypt', status,
396
+ headers, body }`: Payrails' 201, rebuilt from an allow-list, to fulfill the
397
+ paused request with as in `token`. The ciphertext the device made never
398
+ reaches your runtime. Any other answer throws `PaymentOutcomeUnknownError`
399
+ (`payrails_answer_unconfirmed`), and autopilot never runs this mode. See
400
+ [Pay a Payrails checkout](#pay-a-payrails-checkout).
401
+
402
+ Authorize.Net's Direct Post Method form (the merchant's own card form) and
403
+ its SIM hosted form both post to `/gateway/transact.dll`, with the card in
404
+ `x_card_num`, `x_exp_date` and `x_card_code` beside the merchant's signed
405
+ fingerprint. Authorize.Net reads field names without regard to case, and so
406
+ does the SDK: `X_Card_Num` is a card request too. The same URL without a
407
+ card continues untouched: SIM's first step (`x_show_form=PAYMENT_FORM`,
408
+ which loads the hosted form), an eCheck and Visa Checkout. The endpoint is
409
+ the bare URL: a form whose action carries a query string or a fragment is
410
+ not recognized and continues untouched, exactly as the API refuses it. A
411
+ fingerprint that Authorize.Net would find stale (more than 40 minutes old
412
+ at the create, or ahead of the API's clock) is refused for that form only:
413
+ the page reloads with a fresh fingerprint and pays. Authorize.Net spends a
414
+ fingerprint once, so never resubmit a form the cardholder's device sent.
415
+
416
+ - **`card_endpoint`** (a reviewed merchant's own card endpoint). Some
417
+ merchants post the typed card from their own checkout page straight to
418
+ their own server, with no processor in between. Agentcard reviews each one
419
+ and pins a profile in this SDK's copy of the payment core: the checkout
420
+ page's origin, the exact origin, path and query of the card request, the
421
+ session headers and cookies it needs, and the answer headers that may come
422
+ back. The adapters arm every profile's endpoint (`cardEndpointPatterns()`
423
+ lists the `Fetch.enable` patterns) and pause a POST that is exactly a
424
+ profile's card request; a GET, a CORS preflight or a look-alike URL
425
+ continues untouched. While this build's copy of a profile does not take
426
+ payments (all five are in observe today) the request is refused as
427
+ `unsupported_checkout` and the controller reads `unsupported` with reason
428
+ `merchant_card_endpoint_not_enabled`, before any authorization exists. For a
429
+ profile that takes payments, the adapter captures the request's session:
430
+ the profile's own header names from the paused request, the endpoint's
431
+ cookie jar for a cookie profile (`Network.getCookies` or
432
+ `context.cookies()`, since the paused headers never carry it), and a
433
+ `Referer` only when it is a page on the checkout origin. It sends them with
434
+ the paused body, which holds only the dummy card, in the create's
435
+ `merchant_card_endpoint` member, never in `request`, and checks them first
436
+ with the same rules the API and the device run. The cardholder's device
437
+ sends the card request to the merchant itself over Agentcard's checkout
438
+ relay and reports the merchant's answer; `authorize()` resolves with
439
+ `{ mode: 'card_endpoint', profile, status, headers, setCookies, body,
440
+ withheld }`. The adapters write each `Set-Cookie` into your browser
441
+ host-only on the endpoint host and Secure, so none of them travels over
442
+ plain http. A `Set-Cookie` whose `Domain` names a parent of the endpoint
443
+ host that your browser already holds a cookie of that name on is written
444
+ there instead, so it replaces that cookie rather than sitting beside it; one
445
+ whose `Domain` the endpoint host is not inside is dropped, as a browser
446
+ drops it. Then the adapters
447
+ fulfill the paused request with the merchant's status, the profile's
448
+ answer headers, the CORS headers a cross-origin page needs, and the body.
449
+ When the device withheld the answer (it carried the card, or a compressed
450
+ stream the device cannot read), the paused request gets a fixed `502` JSON
451
+ error with code `agentcard_answer_withheld`, and the controller reads
452
+ `outcome_unknown` with reason `merchant_answer_withheld`: the merchant has
453
+ the card, so check the order. Every payment needs the cardholder's
454
+ approval (no Autopilot, no `prepare()`), and the `amount` you pass is the
455
+ only amount authority, so an attachment without one is refused. The
456
+ approval never outlives its request: a card request the page gives up on
457
+ retires the authorization. After the hand-off the attachment holds, so the
458
+ page's retry of the card request, or a step-up that sends the card again,
459
+ is refused until you reconcile the order. Card endpoint events name the
460
+ `profile` and the `endpoint` origin, never the request's URL, body or
461
+ session: the URL carries the merchant's cart and booking ids.
339
462
 
340
463
  `syncRegistry()` asks the API for `SUPPORTED_MODES` only
341
- (`token,cse,hosted_form`), so a processor whose flow this build cannot finish
342
- is never paused; the API serves `hosted_form` entries only to callers that ask.
343
- It also declares `SUPPORTED_CAPABILITIES` (`capture_context`), the dark-launch
344
- half of the same negotiation: a processor gated behind a capability is served
345
- only to a build that declares it and never sits in the built-in list, so it is
346
- paused only after a sync. A registry entry naming a capability this build does
347
- not declare throws `UnsupportedCapabilityError` before creation.
464
+ (`token,cse,hosted_form,device_encrypt`), so a processor whose flow this build
465
+ cannot finish is never paused; the API serves `hosted_form` and `device_encrypt`
466
+ entries only to callers that ask.
467
+ `syncRegistry()` also sends `SUPPORTED_CAPABILITIES`
468
+ (`capabilities=fiserv_card_capture,gmo_shared_pk,capture_context,juspay_txns.v1,datatrans_secure_fields_tokenize,authorize_net_dpm_form,pelecard_transaction,aci_split_card.v1,creditguard_pps_process,primer_payment_instruments,payrails_device_encrypt,converge_hpp_process,worldpay_eprotect_jsonp,wspay_confirm,shiji_sts_pinned_key,amadeus_checkout_actions.v1`),
469
+ the dark-launch half of the same negotiation: the API serves a processor gated
470
+ behind a capability only to a build that declares it, for a company Agentcard
471
+ turned that processor on for, and the built-in list never holds one, so the SDK
472
+ pauses it only after a sync. A registry entry naming a capability this build
473
+ does not declare throws `UnsupportedCapabilityError` before creation. A runtime
474
+ that calls `GET /v2/checkout/recognizers` itself without that parameter, such as
475
+ a native provider integration, never receives the Fiserv, GMO, CyberSource Flex,
476
+ Juspay, Datatrans, Authorize.Net DPM, Pelecard, Primer, Payrails, Worldpay eProtect,
477
+ WSPay, Shiji STS or Amadeus recognizer. See "Prepare a Worldpay eProtect checkout (dark launch)" and "Pay a
478
+ WSPay Components checkout" below.
348
479
  A registry mode this SDK cannot finish throws `UnsupportedModeError` before
349
480
  creation. An approval returned in an unexpected mode has an unknown outcome
350
481
  and holds the attachment for reconciliation. The `authorized` event's detail names
@@ -355,6 +486,71 @@ detail names the `authorizationId`, `submittedAt` and `outcome:
355
486
  one. `amountAuthority` on every replay is `stripe_payment_intent`,
356
487
  `hosted_form_sum` (the form's own amount) or `display_only`.
357
488
 
489
+ ### Shiji STS (MyCheck Wallet V3)
490
+
491
+ La Quinta by Wyndham takes the card through MyCheck's wallet, whose Shiji proxy
492
+ (`sts.proxy.v2.1.js`) encrypts the PAN and the `MM/YY` expiry with RSA PKCS#1
493
+ v1.5 under a key Shiji hardcodes and posts
494
+ `{accessToken, pan, expires, keySequence}` to
495
+ `/v1/token/getTokenId` on `iframe.vault.eu.prod.shijipayment.com` (sandbox:
496
+ `iframe.vault.eu.uat.shijipayment.com`), then the CVV the same way to
497
+ `/v1/token/getCvvId`. The processor ships dark: `syncRegistry()` declares
498
+ `shiji_sts_pinned_key` (`SUPPORTED_CAPABILITIES`), and the API serves the
499
+ recognizer only to an org Agentcard has enabled for it. Every other org's SDK
500
+ recognizes neither URL, and the page's requests go through untouched, as
501
+ before.
502
+
503
+ For an enabled org, both adapters judge each POST to those two URLs before
504
+ anything pauses:
505
+
506
+ - A `getTokenId` in the proxy's own shape is the one prompt. The cardholder's
507
+ device encrypts the real card under the key Agentcard pins (never a key from
508
+ the page), `authorize()` resolves with `substitutions: { encoding: 'json',
509
+ at: '', fields: { pan, expires, cvv } }`, and the adapter writes `pan` and
510
+ `expires` over the page's own at the body root and continues the request
511
+ from the same browser. The `authorized` event names `fields: ['pan',
512
+ 'expires']`. The ciphertext goes only into the request the approval was
513
+ created from, byte for byte. If the page gives up on that request while the
514
+ cardholder decides, the approval is cancelled (`outcome_unknown`, reason
515
+ `merchant_request_aborted`), as for a hosted form: no later `getTokenId`
516
+ is answered from it. Shiji uses one key for every merchant, so a request of
517
+ the same shape could name another merchant's `accessToken` and
518
+ `keySequence`, and the proxy's own request never times out.
519
+ - While Agentcard cannot ask the cardholder for consent to a hotel keeping the
520
+ card, every such `getTokenId` is refused before any create or prompt:
521
+ `unsupported_checkout` with reason `shiji_sts_keep_on_file`, and the
522
+ attachment reads `unsupported`. Nothing reaches Shiji.
523
+ - A `getCvvId` never prompts. It continues with the device's CVV only when it
524
+ follows the `getTokenId` this attachment continued, from the same frame,
525
+ with the same `accessToken`, under a `keySequence` the API reviewed as the
526
+ booking page's own `getCvvId` key and served with the approval, within 120
527
+ seconds, and only once (a `companion_continued` event). Every other
528
+ `getCvvId` is refused as `shiji_sts_companion_unbound`,
529
+ `shiji_sts_companion_expired` or `shiji_sts_companion_used`, so the agent's
530
+ placeholder CVV never rides beside a real card and the real one goes to no
531
+ unreviewed key. An approval served with no reviewed `getCvvId` key resolves
532
+ as `PaymentOutcomeUnknownError` (`cse_substitutions_malformed`) and nothing
533
+ continues. The device's CVV ciphertext is dropped from memory as soon as no
534
+ `getCvvId` can use it: when the attachment is cancelled, the page, the frame
535
+ or the target that sent the `getTokenId` goes away, or that frame loads a new
536
+ document (a reload included), which never obtained the approval. The same
537
+ document changing its path (`history.pushState`), its hash, or following a
538
+ link answered 204 keeps it, even while a navigation it later cancels is
539
+ pending. CDP names the new document (`Page.frameNavigated`); Playwright does
540
+ not, so `attachToPlaywright` reads the frame's `performance.timeOrigin` when
541
+ the `getTokenId` pauses and again for each `getCvvId`, and continues one only
542
+ from the same document.
543
+ - Anything else on those URLs (another body shape, a query string, a field
544
+ encrypted under a larger key) is refused as `unsupported_shiji_sts_request`
545
+ or `shiji_sts_foreign_key`. A body the adapter cannot read is aborted alone
546
+ (`blocked`, `unreadable_shiji_sts_request`).
547
+
548
+ The request names no amount, so pass the booking total as `amount` and
549
+ `currency`: the API refuses a Shiji create without it (`amount_required`).
550
+ Autopilot never pays Shiji. A Shiji set is served once; an approval whose set
551
+ is gone resolves as `PaymentOutcomeUnknownError` with reason
552
+ `cse_substitutions_expired`.
553
+
358
554
  ## Errors worth handling
359
555
 
360
556
  - `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.
@@ -407,7 +603,13 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
407
603
  reporting to the merchant you named (a reusable token in or out, a sale
408
604
  mode that is not a sale, a callback URL off the merchant's host: name the
409
605
  merchant by its hostname when the form carries callback URLs); all three
410
- are terminal for that page. `duplicate_submission` (409,
606
+ are terminal for that page. An Authorize.Net DPM or SIM form adds more
607
+ reasons (`duplicate_field`, `not_a_card_payment`, `relay_response_off`,
608
+ `form_step`, `special_flow`, `merchant_credential`,
609
+ `test_request_on_live_host`, `malformed_field`), terminal the same way,
610
+ and `stale_fingerprint`, which is not: `CheckoutApiError.isStaleFingerprint`
611
+ is true, the adapters quiet the page for `approvalCooldownMs`, and the form
612
+ the page renders after a reload is judged afresh. `duplicate_submission` (409,
411
613
  with `prior_authorization_id` and `prior_status`) means this exact
412
614
  submission already has, or already had, its prompt: the adapters quiet the
413
615
  page's re-posts for `approvalCooldownMs` (no second notification for one
@@ -429,20 +631,52 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
429
631
  page changed, throws `CheckoutPreparationError` with the API's code as its
430
632
  `reason`.
431
633
  - `AmountRequiredError` (`code: 'amount_required'`): the processor's request
432
- names no amount (GMO) and you passed none, so there is nothing for the
634
+ names no amount (GMO, Pelecard, WSPay) and you passed none, so there is nothing for the
433
635
  cardholder to approve. Thrown before anything is created; terminal for that
434
636
  page. Pass `amount` and `currency`.
637
+ - Datatrans refusals at create, all terminal for that page: `CheckoutApiError`
638
+ with `code` `processor_unavailable` (403, Datatrans is not enabled for your
639
+ org), `invalid_datatrans_request`, `datatrans_card_on_file_refused`,
640
+ `datatrans_merchant_unregistered` (the post names a merchant account
641
+ Agentcard has not reviewed), `amount_required` (400; you passed no amount
642
+ above zero and the page total could not be read), or `sandbox_host_on_live_row` and
643
+ `live_host_on_sandbox_row` (400; `pay.sandbox.datatrans.com` takes only a
644
+ test-mode client and `pay.datatrans.com` only a live one). The adapters
645
+ abort the posts behind `invalid_datatrans_request` and, while the served
646
+ card-on-file policy refuses card on file,
647
+ `datatrans_card_on_file_refused` themselves, before any create, and those
648
+ two do not end interception for the page.
435
649
  - `CardEncryptedError`: this processor encrypts the card in-page and its
436
650
  registry entry does not (yet) say the vault can produce that ciphertext;
437
651
  route the purchase to an Agentcard-issued card instead.
438
652
  - `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
439
653
  before an authorization exists. Upgrade. An unexpected approved mode instead
440
654
  raises `PaymentOutcomeUnknownError` and requires reconciliation.
655
+ - `CheckoutPreparationError`: `reason` names why a preparation stopped. For
656
+ CreditGuard, the SDK refuses before any request with `payment_page_required`,
657
+ `unsupported_payment_page`, `payment_page_environment_mismatch` (the page's
658
+ host is not of the environment you named), `payment_page_unsupported` (a
659
+ page URL for another processor) or `merchant_origin_invalid`; the API's
660
+ refusals come through as `creditguard_transaction_refused` (CreditGuard's
661
+ reason in `detail`), `creditguard_transaction_unavailable`,
662
+ `amount_mismatch`, `processor_unavailable`, `sandbox_host_on_live_row` and
663
+ `live_host_on_sandbox_row`. Nothing was created and nothing was charged.
441
664
  - `PreparationRequiredError`: a direct `authorize()` call for a processor that
442
- pays only through a prepared checkout (Fiserv) passed no preparation. Nothing
443
- was created and nothing was charged; call `prepareCheckout()` first. The
444
- adapters refuse such a request before it pauses, with the controller reason
445
- `preparation_required`.
665
+ pays only through a prepared checkout (Fiserv, CreditGuard, Worldpay eProtect)
666
+ passed no preparation. Nothing was created and nothing was charged; call
667
+ `prepareCheckout()` first. The adapters refuse such a request before it
668
+ pauses, with the controller reason `preparation_required`. For Worldpay
669
+ eProtect the refusal is terminal for the attachment: attach again and call
670
+ `prepare()` before the first Pay action.
671
+ - `PaymentOutcomeUnknownError` with reason `creditguard_response_unconfirmed`:
672
+ the approved CreditGuard answer is not an approval of the paused
673
+ transaction, so the page never reads it. Reconcile the order with the
674
+ merchant before another attempt.
675
+ - `PaymentOutcomeUnknownError` with reason `response_expired`: the API no
676
+ longer serves the approved answer. A CreditGuard answer carries
677
+ CreditGuard's card token in its landing url, so the API serves it only for
678
+ 15 minutes after the approval, the default `timeoutMs`. Reconcile the
679
+ order with the merchant before another attempt.
446
680
  - `UnsupportedCapabilityError`: the registry entry is gated behind a
447
681
  dark-launch capability this build does not declare. Only a hand-built
448
682
  registry can hold one. Upgrade.
@@ -454,12 +688,90 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
454
688
  `microform_unsupported` or `invalid_cybersource_request` (400). An approved
455
689
  Flex authorization whose answer is not a transient token raises
456
690
  `PaymentOutcomeUnknownError` with reason `cybersource_flex_token_unconfirmed`.
691
+ - Juspay refusals at create, all terminal for that page: `CheckoutApiError`
692
+ with `code` `processor_unavailable` (403, not enabled for your org),
693
+ `invalid_juspay_request` (400, with a `reason`: the paused `/txns` is not the
694
+ reviewed payment) or `sandbox_host_on_live_row` (400). An approved Juspay
695
+ authorization whose answer is outside the allowlist raises
696
+ `PaymentOutcomeUnknownError` with reason `juspay_response_unconfirmed`, and a
697
+ `ProcessorRefusedError` whose `pspErrorCode` starts with `juspay_tokenize_`
698
+ means the cardholder's device never sent the payment and nothing was charged.
699
+ - The SDK refuses these Juspay requests in the browser, before the API creates
700
+ an authorization and before anything reaches Juspay. It throws nothing for
701
+ them: the attachment's state and its events carry the code.
702
+
703
+ | Code | Where it appears | What it means | What to do next |
704
+ |---|---|---|---|
705
+ | `juspay_request_unsupported` | `getState().reason`; the `unsupported_checkout` event's `reason` names what the SDK refused, such as `save_to_locker` | The vault cannot pay this Juspay request, such as one that saves the card (`save_to_locker=true`) or pays by EMI. A saved card's payment with its stored token passes. | Untick "save card" and pay in full rather than by EMI, then attach again. |
706
+ | `juspay_placeholder_fingerprint` | a `blocked` event | The SDK refuses the page's lookup of the placeholder card. | Nothing: the page carries on without it. |
707
+ | `juspay_unknown_synthetic_token` | a `blocked` event | A `/txns` names a stand-in token this attachment never issued. | Click Pay again on the attached page, so its next tokenize gets a stand-in token from this attachment. |
708
+ | `juspay_request_body_unreadable` | a `blocked` event | The browser does not hand over the card request's body. | Click Pay again: the SDK judges the next request afresh. |
709
+
710
+ `juspay_request_unsupported` holds the attachment as `unsupported` until you
711
+ attach again. Each `blocked` code refuses only its own request, and the
712
+ attachment stays as it was. With `onEvent: console.log` on a Playwright
713
+ attachment, a payment with "save card" ticked logs these three events, and
714
+ `console.log(checkout.getState())` then prints the last object:
715
+
716
+ ```text
717
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
718
+ { type: 'checkout_prepared', detail: { processor: 'juspay' } }
719
+ {
720
+ type: 'unsupported_checkout',
721
+ detail: {
722
+ url: 'https://api.juspay.in/txns',
723
+ method: 'POST',
724
+ processor: 'juspay',
725
+ reason: 'save_to_locker'
726
+ }
727
+ }
728
+ {
729
+ status: 'unsupported',
730
+ authorizationId: null,
731
+ reason: 'juspay_request_unsupported'
732
+ }
733
+ ```
734
+
735
+ A page that sends the three requests the SDK refuses alone logs one `blocked`
736
+ event for each: the placeholder's fingerprint lookup, a `/txns` with a
737
+ stand-in token this attachment never issued, and a POST with no body.
738
+ `console.log(checkout.getState())` then prints the last object:
739
+
740
+ ```text
741
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
742
+ { type: 'blocked', detail: 'juspay_unknown_synthetic_token' }
743
+ { type: 'blocked', detail: 'juspay_request_body_unreadable' }
744
+ { status: 'idle', authorizationId: null }
745
+ ```
746
+ - WSPay refusals at create, all terminal for that page: `CheckoutApiError`
747
+ with `code` `processor_unavailable` (403, WSPay was turned off for your org
748
+ after your last `syncRegistry()`),
749
+ `invalid_wspay_request`, `wspay_keep_on_file_unsupported`, `amount_required`,
750
+ `sandbox_host_on_live_row` or `live_host_on_sandbox_row` (400;
751
+ `proxy-test.wspay.biz` takes only a test-mode client and `proxy.wspay.biz`
752
+ only a live one). The adapters abort the requests behind
753
+ `invalid_wspay_request` and `wspay_keep_on_file_unsupported` themselves, and
754
+ `authorize()` refuses a missing amount, all before any create.
457
755
  - `PlaceholderCardError`: `placeholderCard()` found no such stored card
458
756
  (`card_not_found`), no placeholder for its network (`brand_unsupported`), or
459
757
  no expiry on it (`expiry_unknown`). Nothing was created.
460
758
  - `SubstitutionError`: a `cse` approval could not be written into the paused
461
759
  body (the four encrypted fields were not there). The request is failed and
462
760
  the next one is judged afresh.
761
+ - `CardEndpointRefusedError`: a payment to a reviewed merchant's own card
762
+ endpoint was refused before anything was created or sent. `reason` is
763
+ `not_enabled` (this build's profile does not take payments),
764
+ `amount_required`, `approval_required` (the attachment asks for
765
+ Autopilot), `preparation_unsupported`, or `request_unsupported` (the
766
+ paused request is not the card request Agentcard reviewed; `detail` names
767
+ fields, never values). Every reason but `request_unsupported` holds the
768
+ attachment. The adapters also emit `card_endpoint_refused` with the
769
+ profile, the endpoint origin and the reason.
770
+ - `PageAmountRequiredError` (`code: 'page_amount_required'`, `psp`): the paused
771
+ request names no amount and no page total reached the SDK. For Pelecard the
772
+ total its payment page shows is what ties `amount` to what the page charges,
773
+ so it is required beside `amount`. Thrown before any authorization exists,
774
+ and not terminal: the page's next submission reads the total again.
463
775
 
464
776
  ## Building this package
465
777
 
@@ -641,6 +953,29 @@ Allowed, the request continues untouched, the SDK emits `stripe_payment_continue
641
953
  and the reason becomes `stripe_payment_continued`. One confirm continues per
642
954
  approval; every other follow-up, including an unrelated intent, a changed
643
955
  amount and a retry, stays blocked.
956
+
957
+ A card save can follow a `/v1/payment_methods` approval the same way, once
958
+ Agentcard turns card-save continuations on for your company: the page's
959
+ SetupIntent confirm pays with the approved payment method. A Payment Element
960
+ page that creates the method itself (`paymentMethodCreation: 'manual'`, then
961
+ `confirmSetup({ elements, confirmParams: { payment_method } })`) confirms with
962
+ that method and no card, and the SDK continues the confirm untouched once the
963
+ API allows it. A page whose confirm carries the card again (`confirmCardSetup`
964
+ with the Card Element after `createPaymentMethod`) has it sent with every
965
+ `payment_method_data` field removed and the approved `payment_method` in their
966
+ place: the SDK asks the API with exactly that body and continues the page's
967
+ request with it. The API allows it only for an approval the cardholder gave on
968
+ the Vault page, not by voice, with no `amount` (a SetupIntent charges
969
+ nothing), paid with exactly the approved payment method on the same Stripe
970
+ account, and a SetupIntent Stripe reports still confirmable. Allowed, the SDK
971
+ emits `stripe_setup_continued` with
972
+ `{ authorizationId, setupIntentId, rewritten }`, where `rewritten` is true
973
+ when the SDK wrote the approved payment method in place of a card the confirm
974
+ carried, and the reason becomes `stripe_setup_continued`; the merchant then
975
+ keeps the card for later charges. Until your company is turned on, the API
976
+ refuses the check (`continuation_invalid`) and the confirm is aborted, as
977
+ before. Either kind of intent uses up the approval's one continuation.
978
+
644
979
  Unrecognized merchant-server endpoints remain outside this guard unless listed
645
980
  in `paymentEndpoints`; this is not a guarantee against a merchant charging a
646
981
  saved token on its own server.
@@ -694,6 +1029,8 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
694
1029
  | 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 |
695
1030
  | 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 |
696
1031
  | CyberSource Flex | `production` or `sandbox` | A Microform 2.12.1 card field's `application/jwt` POST to `/flex/v2/tokens` on `flex.cybersource.com` (`production`) or `testflex.cybersource.com` (`sandbox`), sealed under the capture context the field was loaded with |
1032
+ | Worldpay eProtect | `production` or `sandbox` | The new-card registration GET to `/eProtect/paypage` on `request.eprotect.vantivcnp.com` (`production`) or `request.eprotect.vantivprelive.com` (`sandbox`), naming the `paypageId` passed to `prepare()`; see "Prepare a Worldpay eProtect checkout (dark launch)" |
1033
+ | Amadeus Checkout | `production` or `sandbox` | The Checkout Web SDK's single-card `add` POST to `paypages.payment.amadeus.com/1ASIATP/ARIAPP/pay` (`sandbox`: `paypages.test.payment.amadeus.com/1ASIATP`, `1ASIATPU`, `1ASIATPM` or `1ASIATPK`), whose `Amadeus-Checkout-Sdk-Host` header names the prepared page's host |
697
1034
 
698
1035
  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.
699
1036
 
@@ -734,6 +1071,18 @@ After approval, submit the form once. The Vault replaces the card fields on your
734
1071
 
735
1072
  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.
736
1073
 
1074
+ Prepare an Amadeus Checkout card step before the agent clicks Pay:
1075
+
1076
+ ```ts
1077
+ await controller.prepare({ psp: 'amadeus_checkout', environment: 'production' }); // 'sandbox' for Amadeus's test platforms
1078
+ ```
1079
+
1080
+ Amadeus Checkout ships dark: `syncRegistry()` declares `amadeus_checkout_actions.v1`, and the API serves the recognizer only to an org Agentcard enabled for it, so every other org's Amadeus pages run as before. The Checkout Web SDK sends every action to one URL through its tunnel iframe, starting with `load` as the payment step opens. Only the card `add` pauses. `load`, the BIN lookup and every other action continue untouched in every state, and none of them spends or blocks the preparation, so call `prepare()` once the payment step shows. The add binds when its `Amadeus-Checkout-Sdk-Host` header names the prepared page's host; the cardholder's device fills in the card and sends it, and the page's tunnel receives Amadeus's answer. The add names no amount, so the amount you pass is the one the cardholder approves.
1081
+
1082
+ The tunnel gives up on the add 30 seconds after sending it, so the device has 20 seconds from the pause. When they pass with no answer on its way, the SDK asks the API to retire the authorization, or, while the create has not answered, the preparation and any request already bound to it. The API confirms a retirement only while the card is still on the device. Once it does, nothing was sent and nothing can be: the SDK fails the page's request, and the controller reads `failed` with reason `amadeus_send_window_closed`. A prepared checkout is used once, so attach again and prepare again to retry. When the API cannot confirm, the card can already be on its way, so the SDK waits for the device's answer, and the tunnel's own 30 seconds still end the request.
1083
+
1084
+ The SDK refuses some requests itself, before any row or prompt, and the controller reads `failed` with the reason: Air France's and KLM's add, whose expiry their own form keeps (`amadeus_merchant_held_expiry`), a Data Collection Page store until the merchant's field configuration is reviewed (`amadeus_dcp_field_config_unreviewed`), and an add whose Host header names another page (`amadeus_header_binding`). Card saving, split payments, installments, currency conversion and Amadeus 3-D Secure data are refused the same way. The SDK judges an add before a card is chosen, so it cannot see the card's network: an add naming another network reaches the cardholder's device, which refuses it after approval (`vendor_mismatch`), with nothing sent. An add with no preparation is refused with `PreparationRequiredError`. An add that pays with a card the merchant saved carries no card number and continues untouched, and so does Air France's Nuvei payment with a `ccTempToken` after the add. Autopilot never pays an Amadeus payment.
1085
+
737
1086
  Prepare Recurly before clicking the merchant's payment button:
738
1087
 
739
1088
  ```ts
@@ -742,6 +1091,49 @@ await controller.prepare({ psp: 'recurly', environment: 'shared' });
742
1091
 
743
1092
  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.
744
1093
 
1094
+ ### Pay an ACI COPYandPAY checkout
1095
+
1096
+ ACI Worldwide's COPYandPAY widget, which Halfords runs, never sends a card in one request. Each of its card iframes posts only its own field to `https://<host>/v1/checkouts/<id>`, and once all of them answered, the page's own form posts `/v1/checkouts/<id>/payment`. ACI launches dark: `syncRegistry()` declares `aci_split_card.v1`, the API serves the ACI recognizer only to an SDK that declares it, and preparation and create answer `processor_unavailable` until Agentcard enables your org.
1097
+
1098
+ Prepare before Pay. ACI's widget gives up 60 seconds after Pay:
1099
+
1100
+ ```ts
1101
+ await vault.syncRegistry();
1102
+ const controller = await attachToPlaywright(page, { vault, user, merchant, amount: 4210, currency: 'GBP' });
1103
+ await controller.prepare({ psp: 'aci', environment: 'production' }); // 'sandbox' for eu-test.oppwa.com and test.oppwa.com
1104
+ ```
1105
+
1106
+ Type a placeholder card of the approved card's network (a Mastercard-range placeholder for an Agentcard-issued card), then click Pay once. The cardholder's device refuses a placeholder of another network before any card leaves it, and the authorization ends `declined` with reason `aci_brand_mismatch`. The SDK:
1107
+
1108
+ - pauses the card-number request and binds it to the preparation. The cardholder's device sends it to ACI with the real number, CVC and expiry, and the page receives ACI's answer.
1109
+ - holds the page's own CVC and expiry requests and answers them with that answer, so the placeholder values never reach ACI (`aci_leg_absorbed`). They fail when the card-number request fails, or when none begins within 5 seconds, which is what a one-click stored-card form that sends a CVC request alone gets.
1110
+ - lets the holder request through untouched.
1111
+ - lets the page's `/payment` submission continue once, only as the widget's own form post into its hidden iframe (a frame navigation) and only after its checkout's card-number request was answered (`aci_payment_continued`). In classic mode the placeholder expiry the page put there is removed (`stripped: true`). `paymentBrand` must be the brand of the placeholder you typed, and `shopperResultUrl` must be on the prepared merchant origin.
1112
+
1113
+ The SDK removes that expiry only on a browser that sends the body it replaced on a navigation, which the SDK's tests show on Chromium. `attachToCdp` drives Chromium. `attachToPlaywright` reads the page's browser engine, or, for a persistent context, checks whether a CDP session opens, which only Chromium allows. On Firefox or WebKit it refuses a classic-mode `/payment` as `aci_expiry_unstrippable` rather than send the placeholder expiry. A SAQ-A-mode `/payment` has nothing to remove and continues on any engine.
1114
+
1115
+ The `blocked` events the SDK adds for ACI, by `detail`:
1116
+
1117
+ | `detail` | When |
1118
+ |---|---|
1119
+ | `aci_card_on_file_refused` | A request asks ACI to keep the card or pay with a stored one (`createRegistration`, `registrationId`, `registrations[...]`, standing instructions, the `/registration` endpoint). A ready preparation is retired too. |
1120
+ | `aci_unsupported_request` | A leg the widget never sends, or a `/payment` that a page script sends itself instead of the widget's form. |
1121
+ | `aci_checkout_reused` | A second card-number request arrives on a checkout whose first one is running or was answered in this attachment. |
1122
+ | `aci_leg_unanswered` | A CVC or expiry request's card-number request failed, or none began within 5 seconds. |
1123
+ | `aci_payment_unapproved` | A `/payment` arrives before its checkout's card-number request was answered. |
1124
+ | `aci_payment_repeated` | A second `/payment` arrives on the same checkout. |
1125
+ | `aci_brand_mismatch` | `paymentBrand` is not the brand of the placeholder you typed. |
1126
+ | `aci_result_url_refused` | `shopperResultUrl` or `shopOrigin` is off the prepared merchant origin. |
1127
+ | `aci_expiry_unstrippable` | The SDK cannot remove the placeholder expiry: the fields are malformed, or the browser is not Chromium. |
1128
+ | `aci_payment_unsent` | The browser failed to send a `/payment` the SDK continued. The SDK never sends a second one. |
1129
+ | `checkout_interception_not_ready` | A `/payment` arrives after the checkout was cancelled or the attachment stopped. |
1130
+
1131
+ An unprepared card-number request throws `PreparationRequiredError` before any create. Click to Pay, wallet and summary-page forms of the widget are not supported.
1132
+
1133
+ An attachment keeps no memory of an earlier one. The API reads each checkout from ACI before it creates anything, and on ACI's test platform a paid checkout reads as gone: the API answers a card-number request on it with 409 `aci_checkout_not_payable`, and ACI itself refuses a second card-number request and a second `/payment` on it.
1134
+
1135
+ Once the card-number request is answered, ACI holds the real card on that checkout for up to 30 minutes, and the checkout id is what lets anyone post `/payment` against it, so event URLs show it as `{checkoutId}`. The `/payment` check runs in your browser: it holds an honest agent to the approved card, and the merchant's order state is the outcome to confirm. Autopilot is unavailable for ACI.
1136
+
745
1137
  ### Use Checkout Pro in Mexico
746
1138
 
747
1139
  Attach the SDK before entering card fields on `www.mercadopago.com.mx`, then prepare the guest card checkout:
@@ -760,7 +1152,7 @@ Braintree's native `ClientConfiguration` GraphQL query can run before, during or
760
1152
 
761
1153
  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.
762
1154
 
763
- 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.
1155
+ 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 or a Shiji STS 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.
764
1156
 
765
1157
  Lost authorization polling, local approval timeouts, or interrupted browser
766
1158
  handoffs produce `outcome_unknown` and block automatic retry. The thrown
@@ -932,6 +1324,663 @@ const checkout = await attachToPlaywright(page, {
932
1324
  - Autopilot never pays a Flex checkout; the cardholder approves each one on
933
1325
  their device.
934
1326
 
1327
+ ### Pay a Juspay checkout
1328
+
1329
+ Swiggy's website pays cards through Juspay Express Checkout, from the page's
1330
+ own code. The page first sends the card to Juspay's `/card/tokenize`, then pays
1331
+ the order at `/txns` with the token it got back and the security code. A Juspay
1332
+ card token pays any order at that merchant for 15 minutes with no merchant
1333
+ secret, so the agent's browser never holds a real one, and the placeholder the
1334
+ agent typed never reaches Juspay.
1335
+
1336
+ ```ts
1337
+ await vault.syncRegistry(); // the Juspay recognizer is never in the built-in list
1338
+ const checkout = await attachToPlaywright(page, {
1339
+ vault, user: 'your-user-id', merchant: 'Swiggy', amount: 49900, currency: 'INR',
1340
+ });
1341
+ // The agent types a placeholder card and clicks Pay on the merchant's page.
1342
+ ```
1343
+
1344
+ 1. The SDK answers the page's `/card/tokenize` itself with a stand-in token
1345
+ (`ctkn_agentcard` and 32 hex digits) that pays nothing anywhere, and refuses
1346
+ the page's `/v2/card/fingerprint` lookup of the placeholder. Swiggy's page
1347
+ carries on without the fingerprint.
1348
+ 2. The `/txns` that carries that stand-in token pauses, and Agentcard asks the
1349
+ cardholder to approve.
1350
+ 3. On approval, the cardholder's device tokenizes the real card at Juspay and
1351
+ sends the payment itself. The real token never leaves that device.
1352
+ 4. The paused `/txns` receives Juspay's answer, cut down to the order id, the
1353
+ transaction id, the status and the bank's verification redirect. The page
1354
+ then opens the bank's check (3DS or a one-time code) in your browser. Report
1355
+ it with `checkout.requestUserAction('3ds')` and confirm the order with the
1356
+ merchant: an approval is not a payment.
1357
+
1358
+ - `syncRegistry()` declares `juspay_txns.v1`, and the API serves the Juspay
1359
+ recognizer only for an org Agentcard has enabled, so only then does the SDK
1360
+ take over a Juspay request. For any other org the SDK does not take over
1361
+ Juspay's requests, the same as for a processor it does not pay, and a create
1362
+ for a Juspay request answers `403 processor_unavailable`.
1363
+ - Net banking, UPI and wallet payments, a card the shopper saved with the
1364
+ merchant sent with only its stored token and security code, the bank's
1365
+ redirect, CORS preflights and reads pass untouched.
1366
+ - The SDK refuses these before any authorization exists: saving the card
1367
+ (`save_to_locker=true`), `tokenize=true`, EMI, a `/txns` that carries the
1368
+ card number, a saved card's payment that carries a new card's fields, and
1369
+ any card request to `api.juspay.io` or `api-ns1.juspay.in`. The controller
1370
+ reads `unsupported` with reason `juspay_request_unsupported`, and the
1371
+ `unsupported_checkout` event names `processor: 'juspay'` and the `reason`.
1372
+ With `onEvent: console.log` on the attachment above, a payment with "save
1373
+ card" ticked logs these three events, and `console.log(checkout.getState())`
1374
+ then prints the last object:
1375
+
1376
+ ```text
1377
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
1378
+ { type: 'checkout_prepared', detail: { processor: 'juspay' } }
1379
+ {
1380
+ type: 'unsupported_checkout',
1381
+ detail: {
1382
+ url: 'https://api.juspay.in/txns',
1383
+ method: 'POST',
1384
+ processor: 'juspay',
1385
+ reason: 'save_to_locker'
1386
+ }
1387
+ }
1388
+ {
1389
+ status: 'unsupported',
1390
+ authorizationId: null,
1391
+ reason: 'juspay_request_unsupported'
1392
+ }
1393
+ ```
1394
+
1395
+ Untick "save card" and attach again.
1396
+ - The SDK refuses three requests alone, each with a `blocked` event: the
1397
+ fingerprint lookup (`juspay_placeholder_fingerprint`), a `/txns` whose
1398
+ stand-in token this attachment never issued (`juspay_unknown_synthetic_token`),
1399
+ and a card request whose body the browser did not hand over
1400
+ (`juspay_request_body_unreadable`).
1401
+ - Each `/txns` names its own order. If the page gives up on a paused `/txns`
1402
+ and the agent clicks Pay again, the SDK never answers the new order from the
1403
+ earlier approval: the attachment holds as `outcome_unknown` with reason
1404
+ `merchant_request_aborted` until you reconcile the merchant order.
1405
+ - The page decides card eligibility and card offers from the number the agent
1406
+ typed. Type a documented test card of the same network as the stored card,
1407
+ and take no card or bank offers.
1408
+ - Juspay's sandbox host (`sandbox.juspay.in`) takes only a test authorization,
1409
+ paid with a documented test card. The API refuses a live authorization naming
1410
+ it with `400 sandbox_host_on_live_row`.
1411
+ - Juspay's hosted payment page (HyperCheckout, used by IndiGo) sends the card
1412
+ from Juspay's own page, which the SDK does not take over. Autopilot never
1413
+ pays a Juspay checkout; the cardholder approves each one on their device.
1414
+
1415
+ ### Pay a Datatrans Secure Fields checkout
1416
+
1417
+ British Airways takes cards in Datatrans Secure Fields (PCI Proxy). The card
1418
+ number and CVV are Datatrans's own iframes on `pay.datatrans.com`
1419
+ (`pay.sandbox.datatrans.com` in test), and the card-number iframe sends the
1420
+ whole card in one request: `POST /upp/payment/SecureFields/paymentField`,
1421
+ form encoded, in TOKENIZE mode. The SDK pauses that request, the cardholder's
1422
+ device sends the real card to Datatrans, and the iframe receives Datatrans's
1423
+ token (`transactionId`) and its `cardInfo`: brand, credit or debit, consumer
1424
+ or corporate, issuing country and issuer, never a digit of the card.
1425
+
1426
+ ```ts
1427
+ await vault.syncRegistry(); // the Datatrans recognizer is never in the built-in list
1428
+ const checkout = await attachToPlaywright(page, {
1429
+ vault, user: 'your-user-id', merchant: 'British Airways', amount: '189.90', currency: 'GBP',
1430
+ });
1431
+ ```
1432
+
1433
+ - **Dark launch.** `syncRegistry()` declares the
1434
+ `datatrans_secure_fields_tokenize` capability, and the API serves the
1435
+ Datatrans recognizer only to an org Agentcard has enabled for it. Every
1436
+ other org's SDK leaves Datatrans requests alone, as before.
1437
+ - **Pass the amount.** The card request names no amount: the merchant charges
1438
+ the token later, from its own server. The `amount` and `currency` you pass,
1439
+ or the page total when you pass none, are what the approval shows. Without
1440
+ either the create answers `amount_required`.
1441
+ - **What pauses.** Only the card-number iframe's TOKENIZE post of a new card.
1442
+ A post Datatrans's own iframe would not send for that flow (another mode, a
1443
+ payment or 3-D Secure post, a key the iframe never writes, a URL variant, a
1444
+ content type other than jQuery's form type) is aborted with a `blocked`
1445
+ event, detail `invalid_datatrans_request`. Nothing is created, nobody is
1446
+ prompted, the controller's state does not change, and the page's next post
1447
+ is judged afresh. Wallet, bank-account and CVV-only posts carry no card
1448
+ number and continue untouched, and so do the iframe documents and the BIN
1449
+ lookup (`paymentField/cardInfo`).
1450
+ - **Card on file.** A post with `usage=REGISTRATION` asks PCI Proxy for a
1451
+ reusable alias and a network token, so the merchant could charge the card
1452
+ again without another approval. The API serves Agentcard's card-on-file
1453
+ policy with the Datatrans recognizer (`cardOnFile`), and the SDK reads it at
1454
+ each `syncRegistry()`. While it is `refuse`, as it is today, the post is
1455
+ aborted with a `blocked` event, detail `datatrans_card_on_file_refused`,
1456
+ before any create. British Airways's Nexus checkout sends it for every new
1457
+ card, so a Nexus checkout is refused this way for now. When the policy
1458
+ allows card on file, the post pauses like any other and the approval
1459
+ applies the policy; no SDK release is needed.
1460
+ - **The same form, submitted again.** British Airways submits the form again
1461
+ whenever a field loses focus or the expiry changes. Once a post was
1462
+ approved, a later post from the same iframe that differs from it only in
1463
+ the expiry (`expm`, `expy`) and the browser details is answered with the
1464
+ same token and `cardInfo`: no second prompt, no second card sent. The
1465
+ adapters emit `repeat_answered` with the `authorizationId`; it is not a
1466
+ second payment and must not be counted as one. Any other change (a new
1467
+ `formId` from a reloaded iframe, another placeholder card or CVV, another
1468
+ email or phone number) is a new approval. The answer lasts while that
1469
+ approval is the checkout's current one, up to the token's 30-minute
1470
+ lifetime: once the order completes or fails, the attachment stops, or
1471
+ another authorization starts, the form is judged again like any card post.
1472
+ It also ends once the form has posted nothing for five minutes. Once the
1473
+ token reached the page, the merchant can already hold an order paid with
1474
+ it, and the post names no amount or order, so before each later re-submit
1475
+ the SDK calls your `resolveMerchantResult` and answers only while it
1476
+ reports `pending`; that call changes no checkout state. A completed,
1477
+ failed or unknown order, a resolver that throws, or one that has not
1478
+ answered within five seconds makes the re-submit a new question, and an
1479
+ attachment with no `resolveMerchantResult` answers no
1480
+ re-submit after the token reached the page: each one asks the cardholder
1481
+ again.
1482
+ - **Type a placeholder of the card's brand.** The iframe names the brand
1483
+ (`paymentMethod`) from the digits the agent types, and British Airways's page
1484
+ sets its CVV length and contact fields by that brand, so the cardholder's
1485
+ device refuses a card whose brand differs, before anything is sent.
1486
+ Agentcard-issued cards are Mastercard: type a Mastercard test number.
1487
+ - **Merchant accounts.** A token minted into a Datatrans account can be read
1488
+ back as the card number by that account's holder, so the API refuses a
1489
+ merchant account Agentcard has not reviewed (`datatrans_merchant_unregistered`).
1490
+ - Autopilot never pays a Datatrans checkout; the cardholder approves each one
1491
+ on their device. `pay.sandbox.datatrans.com` takes only a test-mode client
1492
+ and `pay.datatrans.com` only a live one.
1493
+
1494
+ ### Pay a Pelecard checkout
1495
+
1496
+ Pelecard's hosted payment page (Iframe/Redirect 2.0) runs at
1497
+ `https://gateway21.pelecard.biz/PaymentGW?transactionId=...` (`gateway20` on
1498
+ older terminals), in the merchant's iframe or top level. When the buyer clicks
1499
+ Pay, the page sends the card in one XHR to its own `/PaymentGW/Transaction`.
1500
+ The SDK pauses that request, the cardholder approves on their phone, their
1501
+ device sends the real card to Pelecard, and the page receives Pelecard's
1502
+ answer, so its own script takes the buyer to the merchant's landing page.
1503
+
1504
+ Pelecard ships dark: `syncRegistry()` asks for the `pelecard_transaction`
1505
+ capability, and the API serves the recognizer only to an org Agentcard has
1506
+ enabled for Pelecard, in test mode only. For every other org the SDK leaves
1507
+ Pelecard's requests alone.
1508
+
1509
+ ```ts
1510
+ await vault.syncRegistry();
1511
+ const checkout = await attachToPlaywright(page, {
1512
+ vault, user: 'user_123', merchant: 'Uman tourist tax',
1513
+ amount: 2700, currency: 'usd', // required: the request names no amount
1514
+ pageAmount: async () => {
1515
+ // Required: the total Pelecard's page shows, which the API holds against `amount`.
1516
+ const frame = page.frames().find((f) => /^https:\/\/gateway2[01]\.pelecard\.biz\/PaymentGW\b/.test(f.url()));
1517
+ const shown = await frame?.evaluate(() => {
1518
+ const p = (globalThis as any).generalParameters;
1519
+ return p ? { total: p.total, currencyCode: p.currencyCode } : null;
1520
+ });
1521
+ if (typeof shown?.total !== 'number' || typeof shown?.currencyCode !== 'string') return undefined;
1522
+ return { amount: Math.round(shown.total * 100), currency: shown.currencyCode.toLowerCase() }; // two-decimal currencies
1523
+ },
1524
+ });
1525
+ ```
1526
+
1527
+ - Pass `amount` and `currency`. Pelecard bound the total, the currency and
1528
+ the action (a charge, a card registration or a hold) to the payment page
1529
+ when the merchant created it, so the submission names none of them. The
1530
+ pair you pass is what the cardholder approves; without it the submission
1531
+ fails with `AmountRequiredError` before anything is created. No Pelecard
1532
+ answer states an amount either, so confirm the order with the merchant.
1533
+ - Pass the page's total as `pageAmount`. It is the one figure that ties
1534
+ `amount` to what the merchant bound to the payment page, so a submission
1535
+ without it fails with `PageAmountRequiredError` before anything is created,
1536
+ and the page's next submission reads it again. The API refuses a create
1537
+ without it (`page_amount_required`), a total of 0 (a card registration or a
1538
+ hold of an amount the page does not show) and a total that disagrees with
1539
+ `amount` (`amount_mismatch`).
1540
+ - Before any prompt, the SDK refuses a submission that asks Pelecard to keep
1541
+ the card on file (the buyer ticked "save my card", or the body sets
1542
+ `CreateToken`) as `pelecard_keep_on_file_unsupported`: the merchant could
1543
+ charge that card again without another approval, and the approval cannot
1544
+ ask the cardholder yet. Anything else the page's one-time card payment
1545
+ never sends (a saved token, a card track, a phone authorization,
1546
+ installments, a buyer-typed total, a key the page does not build) is
1547
+ `invalid_pelecard_request`. Both arrive as a `blocked` event, the page's
1548
+ request is aborted, nothing is created, and the page's next submission is
1549
+ judged afresh. A merchant can still set `CreateToken` when it creates the
1550
+ payment page, which no request shows.
1551
+ - The page's other tabs post to the same URL with no card (Bit, wallets, the
1552
+ PIN pad, bank transfer) and continue untouched.
1553
+ - The page waits up to ten minutes for the answer, and Pelecard's payment
1554
+ session is expected to last about as long from page load, so the approval
1555
+ has to land inside it. A 3-D Secure answer is never read as approved:
1556
+ reconcile that payment with the merchant.
1557
+
1558
+ ### Pay a CreditGuard checkout
1559
+
1560
+ CreditGuard's hosted payment page (Hyp PPS, `pps.creditguard.co.il`; SeboCalm frames it) sends the card in one request, `POST /services/v1/txn/{txId}/process`, and that request is the sale itself: it names neither the amount nor the merchant. So a CreditGuard payment always starts with a preparation that names the page the checkout framed, the iframe's own address:
1561
+
1562
+ ```ts
1563
+ const pps = page.frames().find(frame => /^https:\/\/(pps|pps2|ppsuat)\.creditguard\.co\.il\/\?/.test(frame.url()));
1564
+ await controller.prepare({ psp: 'creditguard', environment: 'production', paymentPageUrl: pps!.url() });
1565
+ // 'sandbox' for CreditGuard's UAT page on ppsuat.creditguard.co.il
1566
+ ```
1567
+
1568
+ Agentcard reads that transaction from CreditGuard before the cardholder is asked. Pass the `amount` and `currency` CreditGuard holds (ILS in agorot: `24000` is 240.00 ILS); another pair is refused as `amount_mismatch`. A page the Vault cannot finish (3-D Secure, OTP, currency conversion, installments, a required Israeli ID, or a form that hides a card field or asks for a field the cardholder's device does not fill) is refused as `creditguard_transaction_refused`, with CreditGuard's reason in `detail`. The merchant page must be HTTPS under a dotted hostname, because every submit names it.
1569
+
1570
+ After the approval, click the page's Pay once. Only the page's own new-card submit for that transaction pauses: the cardholder's device sends the real card to CreditGuard, and the page reads CreditGuard's answer, rebuilt from payment-core's allow-list, then moves on to the merchant's landing page. The page's card lookup, its transaction read and every other request continue untouched with your placeholder, so type a foreign test number.
1571
+
1572
+ A submit the device would not fill is aborted before anything is created, with a `blocked` event `{ reason: 'creditguard_request_unsupported', refusal }`: `keep_on_file` ("save my card"), `installments`, `dcc`, `personal_id`, `saved_card`, `wallet` and the like. The preparation stays ready, so untick the option and click Pay again. Without a ready preparation the submit is refused with `PreparationRequiredError`. CreditGuard's own 3-D Secure call to Nuvei, from its page or from any frame inside it, is refused, and the attachment reads `unsupported` with reason `creditguard_three_d_secure`; it is never approved as a Nuvei payment. Autopilot never pays CreditGuard.
1573
+
1574
+ ### Pay a Primer checkout
1575
+
1576
+ Primer launches dark: `syncRegistry()` declares the
1577
+ `primer_payment_instruments` capability (in `SUPPORTED_CAPABILITIES`), and the API
1578
+ serves the Primer recognizer only to an org Agentcard has enabled for Primer.
1579
+ For every other org nothing changes: Primer's requests are never paused and
1580
+ never refused.
1581
+
1582
+ For an enabled org, the SDK pauses one request: the card tokenization that
1583
+ Primer Web SDK v2 (Printful loads v2.63.0) sends from its hidden api-controller
1584
+ frame on `https://sdk.primer.io` when the shopper pays with a new card. That is a
1585
+ `POST` to `https://sdk.api.primer.io/payment-instruments` (sandbox
1586
+ `sdk.api.sandbox.primer.io`) as JSON, with the card at
1587
+ `paymentInstrument.number`. The frame is cross-origin, and both adapters pause
1588
+ the request inside it. The cardholder's device fills the real card into the
1589
+ paused body and sends it to Primer itself. The page receives only Primer's
1590
+ answer: one single-use card token, rebuilt from an allow-list and handed over
1591
+ under a JSON content type. Any other answer on an approved authorization, such
1592
+ as a multi-use token or a saved card, throws `PaymentOutcomeUnknownError` with
1593
+ reason `primer_token_unconfirmed`.
1594
+
1595
+ ```ts
1596
+ await vault.syncRegistry();
1597
+ const controller = await attachToPlaywright(page, {
1598
+ vault, user: 'usr_123', merchant: 'Printful',
1599
+ amount: 2599, currency: 'usd', // Primer's request names no amount
1600
+ });
1601
+ ```
1602
+
1603
+ Primer's request names no amount: Primer's SDK or the merchant's server charges
1604
+ the token later. Pass `amount` and `currency`, or let the page total
1605
+ (`pageAmount`, or `[data-agentcard-amount]`) name the price: that is what the
1606
+ cardholder approves. With neither, the API answers 400 `amount_required` before
1607
+ anything is created, and the attachment reads `failed`. A 3-D Secure challenge
1608
+ that follows the token runs in your browser: call `requestUserAction('3ds')`
1609
+ when you see one.
1610
+
1611
+ Primer's SDK stamps `preferredNetwork` from the digits typed, and the
1612
+ cardholder's device refuses one that is not the paying card's own network. Type
1613
+ a placeholder of the same network as the card that pays.
1614
+
1615
+ The other requests on Primer's hosts:
1616
+
1617
+ - A wallet tokenization with no card (PayPal, Apple Pay, Google Pay, Klarna) and
1618
+ a saved-card exchange continue untouched.
1619
+ - A tokenization that asks Primer to save the card (`tokenType: "MULTI_USE"` or
1620
+ `paymentFlow: "VAULT"`) is aborted alone, with a `blocked` event,
1621
+ `primer_keep_on_file`. The attachment is not held: clear the page's save-card
1622
+ choice and pay again.
1623
+ - A card request from Primer Checkout (`@primer-io/primer-js`), whose frame gives
1624
+ up on its own request after 30 seconds, is aborted as `unsupported_checkout`
1625
+ with reason `unsupported_primer_sdk`, and any other card request the v2 frame
1626
+ would not send with `unsupported_primer_request`. The attachment reads
1627
+ `unsupported` with that reason and holds, since the merchant's integration
1628
+ decides that shape.
1629
+ - A request whose body the SDK cannot read is aborted alone, with a `blocked`
1630
+ event, `unreadable_primer_request`.
1631
+
1632
+ A client session the merchant created to save every card after payment (Printful
1633
+ does this for a signed-in shopper) shows nothing in the request, so the SDK
1634
+ cannot detect that save. Until the cardholder can consent to it, Primer pays
1635
+ only for a test-mode client: for a live client the API answers 400
1636
+ `invalid_primer_request` with reason `live_keep_on_file` before anything is
1637
+ created, the SDK aborts the paused request with nothing sent, and the attachment
1638
+ reads `failed`.
1639
+
1640
+ ### Pay a Payrails checkout
1641
+
1642
+ Payrails (web SDK v6 secure fields, as on Blacklane's consumer booking checkout)
1643
+ launches dark: `syncRegistry()` asks for the `device_encrypt` mode and declares
1644
+ the `payrails_device_encrypt` capability, and the API serves the Payrails
1645
+ recognizer only to an org Agentcard has enabled for Payrails. For every other
1646
+ org nothing changes: no answer is read, no Payrails host is intercepted, and
1647
+ Payrails' requests go through untouched.
1648
+
1649
+ For an enabled org, the SDK pauses one request: the card form's save, a `POST`
1650
+ to the session's `saveInstrument` link (`https://api-pub.payrails.io/public/payment/instruments`;
1651
+ test hosts `api-pub.staging.payrails.io` and `sandbox-api-pub.staging.payrails.io`)
1652
+ that carries the whole card as one JWE. The key it was encrypted under is not in
1653
+ that request, so the adapters read the Payrails init payload (`{version, data}`)
1654
+ from the page's own XHR and fetch answers and keep, in memory only, up to eight
1655
+ sessions for fifteen minutes. A paused save binds to the one session whose token,
1656
+ save link, `holderReference` and `providerConfigId` it names, seen at most nine
1657
+ minutes before, and that session's key rides on the create. The cardholder's
1658
+ device encrypts the real card under it and sends the save to Payrails itself;
1659
+ the page receives Payrails' 201 rebuilt from an allow-list (the instrument, its
1660
+ status, the network, the BIN cut to six digits, the last four, the expiry, the
1661
+ holder name and the fingerprint). Call `syncRegistry()` before attaching, or the
1662
+ page's session answers are not read.
1663
+
1664
+ ```ts
1665
+ await vault.syncRegistry();
1666
+ const controller = await attachToPlaywright(page, {
1667
+ vault, user: 'usr_123', merchant: 'Blacklane',
1668
+ amount: 8900, currency: 'eur', // the save names no amount: this, or the page total, is what the cardholder approves
1669
+ });
1670
+ ```
1671
+
1672
+ A save names no amount (the merchant charges the saved card later), so pass
1673
+ `amount` and `currency`, or a `pageAmount` reader: without either, the save
1674
+ throws before anything is created.
1675
+
1676
+ What the SDK refuses, before anything is created, with the reason on the
1677
+ `unsupported_checkout` event:
1678
+
1679
+ - A save that keeps the card on file (`storeInstrument: true` or
1680
+ `UnscheduledCardOnFile`, which Blacklane sends on every save):
1681
+ `payrails_card_on_file_consent_required`, until the cardholder can consent to
1682
+ it. A `Subscription` save (`payrails_future_usage_refused`) and a body that is
1683
+ not the Payrails SDK's own (`payrails_request_malformed`,
1684
+ `payrails_not_card_save`, `payrails_jwe_malformed`) are refused too. The
1685
+ attachment reads `unsupported` with that reason and holds, since the merchant's
1686
+ integration decides that shape.
1687
+ - Paying with the card form or a stored card with a CVC field, which sends the
1688
+ card inside `paymentComposition` to any Payrails host:
1689
+ `unsupported_payrails_card_payment`. The attachment holds, and the agent's
1690
+ placeholder card never reaches an authorization.
1691
+ - A save no captured session matches (`payrails_session_unavailable`), or whose
1692
+ captured key is not the one the page's secure frame used (`payrails_key_invalid`,
1693
+ `payrails_key_mismatch`): that request alone is aborted, the event carries
1694
+ `message: 'Payrails session expired, reload the checkout'`, and the save after
1695
+ a reload is judged afresh.
1696
+
1697
+ A save with no card (a dynamic payment-method form posts to the same link)
1698
+ continues untouched. A `POST` to a Payrails host with no body the SDK can read,
1699
+ whether it has none, an empty one or a streamed one, is aborted alone with a
1700
+ `blocked` event, `unreadable_payrails_request`, by both adapters: Playwright
1701
+ reports all three the same way. The Payrails SDK sends a JSON body with every
1702
+ `POST`, so none of its own requests is refused this way. Payrails' static host,
1703
+ `assets.payrails.io`, serves only files and takes no payment, so a bodyless
1704
+ `POST` another script sends there continues untouched. The matching API,
1705
+ Vault and database releases are required, and the API also refuses a save for a
1706
+ merchant whose Payrails workspace Agentcard has not reviewed.
1707
+
1708
+ ### Pay an Elavon Converge payment page
1709
+
1710
+ Elavon Converge's Hosted Payment Page is where NY DMV's payment apps send the
1711
+ browser: Converge's own page at
1712
+ `https://api.convergepay.com/hosted-payments/?ssl_txn_auth_token=...`, at top
1713
+ level. When the buyer clicks Pay, the page posts the card as one JSON request
1714
+ to its own `/hosted-payments/service/payment/hpp/process`. The SDK pauses that
1715
+ request, the cardholder approves the amount it names on their phone, their
1716
+ device sends the real card to Converge, and the page receives Converge's
1717
+ approval, so its own script shows the receipt and returns the buyer to the
1718
+ merchant. Converge's demo environment serves the same page on
1719
+ `api.demo.convergepay.com`, which pays in test mode only.
1720
+
1721
+ Converge ships dark: `syncRegistry()` asks for the `converge_hpp_process`
1722
+ capability, and the API serves the recognizer only to an org Agentcard has
1723
+ enabled for Converge. For every other org the SDK leaves Converge's requests
1724
+ alone.
1725
+
1726
+ ```ts
1727
+ await vault.syncRegistry();
1728
+ const checkout = await attachToPlaywright(page, {
1729
+ vault, user: 'user_123', merchant: 'NY DMV',
1730
+ // No amount: the request names its own (ssl_amount, US dollars), and that is what the cardholder approves.
1731
+ });
1732
+ ```
1733
+
1734
+ - No `amount` is needed. The request's `ssl_amount` is the amount the
1735
+ cardholder approves; an `amount` and `currency` you pass must agree with it.
1736
+ - Every Converge payment waits for the cardholder's own approval. An autopilot
1737
+ grant never pays one.
1738
+ - Electronic check and financing payments on the same URL carry no card and
1739
+ continue untouched.
1740
+ - Before any prompt, the SDK refuses every other shape on that URL with
1741
+ payment-core's own code (`ConvergeRefusalReason`, listed in
1742
+ `CONVERGE_REFUSAL_REASONS`), the one the API and the cardholder's device
1743
+ would answer: a wallet, the debit, gift card and push-to-card tabs, a
1744
+ follow-up after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
1745
+ public-sector service fee, recurring or installment fields, card saving, a
1746
+ tip or cash back, another currency, no amount, no page session, and anything
1747
+ that is not the page's own request. The page's request is aborted, nothing is
1748
+ created, and the attachment is not held, so the page's next payment is judged
1749
+ afresh. The table below gives each code's meaning and the next step.
1750
+ - A terminal with 3-D Secure 2, Pay Defense or a surcharge answers the first
1751
+ request with a step instead of a result. The Vault never hands that answer to
1752
+ the page: the authorization ends declined as a processor refusal with
1753
+ `converge_step_unsupported` and nothing charged, `authorize()` throws
1754
+ `ProcessorRefusedError` with that `pspErrorCode`, and the page shows its own
1755
+ decline.
1756
+ - One payment per payment page. A second authorization for the same page
1757
+ session or invoice gets `duplicate_submission` for 15 minutes after the
1758
+ first card could last have left the device: confirm the payment with the
1759
+ merchant before trying again.
1760
+
1761
+ A refused payment reaches your `onEvent` as a `blocked` event whose `detail` is
1762
+ the code. This is what `onEvent` received when the agent paid with Google Pay
1763
+ on Converge's page, printed by the SDK's own Chromium test (in
1764
+ `packages/checkout`: `npm run build`, then
1765
+ `node elavon-converge-browser.test.mjs "playwright Google Pay"`):
1766
+
1767
+ ```text
1768
+ PASS playwright Converge Google Pay, then the card form; onEvent received {"type":"blocked","detail":"converge_wallet_refused"}
1769
+ 1 local Chromium Converge scenarios passed; no Converge host, demo account or live merchant was contacted.
1770
+ ```
1771
+
1772
+ The page's own request then fails the way a dropped connection does (its XHR
1773
+ ends with status 0), and the page keeps its session, so the buyer can pay with
1774
+ the card form next.
1775
+
1776
+ #### Converge refusal codes
1777
+
1778
+ The first 22 codes arrive as the `blocked` event's `detail`, before any
1779
+ authorization exists, and the API answers a create that carries one with a 400
1780
+ of the same code. `converge_step_unsupported`, `converge_declined` and
1781
+ `converge_session_timeout` arrive after the cardholder approved, as
1782
+ `ProcessorRefusedError.pspErrorCode`. `duplicate_submission` is the create's
1783
+ 409, as `CheckoutApiError.code`.
1784
+
1785
+ | Code | What it means | What to do next |
1786
+ |---|---|---|
1787
+ | `converge_wallet_refused` | The buyer paid with a wallet (Google Pay, Apple Pay or Visa Checkout), so the card number came from the wallet, not from a card the cardholder approves. | Pay with the page's card form; the next payment is judged afresh. |
1788
+ | `converge_debit_card_refused` | The payment came from the page's debit card tab. | Pay with the credit card form. |
1789
+ | `converge_gift_card_refused` | The payment came from the page's gift card tab. | Pay with the credit card form. |
1790
+ | `converge_push_to_card_refused` | The payment would send money to the card instead of charging it. | None: an Agentcard card never receives money. |
1791
+ | `converge_payment_type_refused` | The body names a payment type other than a credit card sale, or an electronic check or financing payment carries a card number. | Pay with the credit card form. |
1792
+ | `converge_captcha_refused` | The page sent a CAPTCHA answer with the payment. | The buyer pays this merchant without Agentcard: the cardholder's device cannot answer a CAPTCHA. |
1793
+ | `converge_service_fee_refused` | The merchant adds a public-sector service fee, which Converge charges as a second payment. | The buyer pays this merchant without Agentcard: a one-time card cannot pay the second charge. |
1794
+ | `converge_3ds_follow_up_refused` | The body follows a 3-D Secure or Pay Defense step the page already ran. | Start a new checkout from the merchant's page. A terminal that runs the step on every payment cannot be paid through Agentcard yet. |
1795
+ | `converge_surcharge_refused` | The body follows a surcharge quote, or skips it with the debit bypass. | Start a new checkout from the merchant's page. A surcharging terminal cannot be paid through Agentcard yet. |
1796
+ | `converge_amount_add_on_refused` | The payment adds a tip or cash back to `ssl_amount`. | Pay again without the tip or cash back. |
1797
+ | `converge_card_saving_refused` | The body asks Converge to keep the card (`ssl_get_token` or `ssl_add_token`), or pays with a card Converge stored (`ssl_token`). | Pay again with the typed card and card saving off. |
1798
+ | `converge_recurring_refused` | The body sets up recurring or installment charges. | Pay the amount as one payment. |
1799
+ | `converge_currency_refused` | The payment is in a currency other than US dollars. | The buyer pays this one without Agentcard: Agentcard pays Converge in US dollars only. |
1800
+ | `converge_amount_missing` | The body names no amount it can read, or an amount of zero. | Start a new checkout from the merchant's page. |
1801
+ | `converge_context_missing` | The body carries no payment page session (`context`). | Start a new checkout from the merchant's page. |
1802
+ | `converge_body_malformed` | The body is not the page's own JSON, byte for byte, or carries a key the page never writes. | Let the page send its own request; never rewrite it. |
1803
+ | `converge_credential_refused` | The body names Converge account credentials, which the page never sends. | Let the page send its own request. |
1804
+ | `converge_card_missing` | The body carries no card number. | Type the placeholder card into the page's own card fields. |
1805
+ | `converge_card_malformed` | The card number, expiry or security code is not in the page's own format. | Type the placeholder card into the page's own card fields. |
1806
+ | `converge_url_refused` | The request is not sent to the exact `hpp/process` URL. | Let the page send its own request. |
1807
+ | `converge_method_refused` | The request is not a POST. | Let the page send its own request. |
1808
+ | `converge_content_type_refused` | The request's content type is not JSON. | Let the page send its own request. |
1809
+ | `converge_step_unsupported` | After the approval, Converge answered with a 3-D Secure 2, Pay Defense, currency, surcharge or debit bypass step, which comes before any authorization. Nothing was charged. | Confirm with the merchant, then have the buyer pay without Agentcard: the Vault cannot complete that step yet. |
1810
+ | `converge_declined` | Converge declined the card. | Check the merchant's payment status, then start a new checkout to try again. |
1811
+ | `converge_session_timeout` | Converge's payment page session ended before the payment. Nothing was charged. | Start a new checkout from the merchant's page. |
1812
+ | `duplicate_submission` | An earlier authorization holds this payment page session or invoice: it awaits the cardholder, was approved, or ended without a recorded outcome. | Wait for that authorization, or confirm the payment with the merchant before trying again. |
1813
+
1814
+ ### Prepare a Worldpay eProtect checkout (dark launch)
1815
+
1816
+ Worldpay eProtect (Vantiv, Litle PayPage) support ships dark. Worldpay's script
1817
+ encrypts the card in the page and sends it as the
1818
+ query of one JSONP `<script>` GET to `/eProtect/paypage` on
1819
+ `request.eprotect.vantivcnp.com`, or on `request.eprotect.vantivprelive.com`
1820
+ (Worldpay's pre-live host, the `sandbox` environment), then reads the answer
1821
+ from the script's own callback. `syncRegistry()` declares the
1822
+ `worldpay_eprotect_jsonp` capability, and the API serves the eProtect
1823
+ recognizer to a build that declares it only for an org Agentcard has enabled.
1824
+ For every other org the API serves no eProtect recognizer and answers
1825
+ `processor_unavailable` to an eProtect preparation or create, so the SDK leaves
1826
+ eProtect traffic exactly as it found it, as it does before a sync.
1827
+
1828
+ The merchant's script gives up on the registration 10 to 15 seconds after Pay,
1829
+ and re-sends it to a failover host after 5, which is too soon for a prompt that
1830
+ waits on a person. An eProtect checkout is therefore always prepared:
1831
+
1832
+ ```ts
1833
+ const checkout = await attachToPlaywright(page, {
1834
+ vault, user: 'your-user-id', merchant: 'Example merchant',
1835
+ amount: 4210, currency: 'usd',
1836
+ cardId: 'vc_...', // required: the stored card that pays
1837
+ onApprovalUrl: deliverPrivatelyToCardholder,
1838
+ });
1839
+ await checkout.prepare({ psp: 'worldpay_eprotect', environment: 'production', paypageId: 'MerchantPaypageId' });
1840
+ await page.getByRole('button', { name: 'Place order', exact: true }).click();
1841
+ ```
1842
+
1843
+ - `paypageId` is the merchant's eProtect paypageId: the Worldpay merchant the
1844
+ registration is for, which is also the merchant Worldpay lets charge the card
1845
+ again. It is fixed before the page sends anything, so a registration naming
1846
+ another paypageId never spends the approval. The Worldpay iframe names it in
1847
+ its URL (`/eProtect/payframe_<paypageId>_...`); a JS API merchant carries it
1848
+ in its eProtect configuration.
1849
+ - `cardId` is required, and the approval is locked to that card. Worldpay never
1850
+ receives an expiry: the merchant sends the one typed into its form with the
1851
+ registration. The agent types a placeholder number of the stored card's
1852
+ network, the security code `000`, and the stored card's real expiry
1853
+ (`expiry_month` and `expiry_year` from `GET /v2/vault_cards`) before it
1854
+ prepares.
1855
+
1856
+ After Pay, the SDK pauses the registration and binds it to the preparation.
1857
+ The cardholder's device, still on the approval page, encrypts the approved card
1858
+ under Worldpay's key into that same query and sends it through Agentcard's
1859
+ encrypted relay. The SDK then answers the paused script as Worldpay would:
1860
+ the registration in the script's own callback, under Worldpay's script type,
1861
+ with only the fields the merchant needs (no first six or last four digits).
1862
+ The SDK reads the authorization every 500 ms for 15 seconds, so the answer
1863
+ lands inside the merchant's window. A registration is not a payment: the
1864
+ merchant authorizes it with the typed expiry, so confirm the order with the
1865
+ merchant.
1866
+
1867
+ | Request on eProtect's hosts | What the adapters do |
1868
+ | --- | --- |
1869
+ | The registration GET naming the prepared `paypageId`, on the prepared environment's host | Paused, bound to the preparation and answered |
1870
+ | A registration with no ready preparation | Refused locally with `PreparationRequiredError`; nothing is created |
1871
+ | A registration for another `paypageId` or host | Refused, and the preparation is retired (`checkout_changed`) |
1872
+ | The vendor's re-send with `targetServer=secondary` (`secondary.eprotect.vantivcnp.com`, or the pre-live host itself) of a registration the attachment saw | Aborted quietly (`blocked` event, `eprotect_failover_aborted`); it never prompts or claims a preparation |
1873
+ | Any other request on `/eProtect/paypage`: CVV-only, PIN, wallet or private-label variants, the native-app Mobile API form post, a re-send of nothing the attachment saw | Aborted as `unsupported_checkout` |
1874
+ | `/eProtect/ppstats` reporting `900` or `901` for the running registration | The report continues, and the approval ends as `merchant_request_aborted` (event `merchant_request_abandoned`) |
1875
+ | The vendor's scripts, the payframe and every other report | Continue untouched |
1876
+
1877
+ Worldpay's script sends `900` or `901` when the merchant's timeout passed with
1878
+ no answer. The SDK then cancels the authorization, which the API confirms only
1879
+ before the device sent the registration, and fails the paused script instead of
1880
+ answering it. When the answer had already reached the page, the controller
1881
+ moves to `outcome_unknown`.
1882
+
1883
+ `prepare()` refuses locally with `CheckoutPreparationError` reason
1884
+ `paypage_id_invalid` (missing or not 1 to 64 letters and digits, or passed for
1885
+ another processor) or `card_required`, and with `ready_unconfirmed` when the
1886
+ API confirms another paypageId or card. A live checkout (`environment:
1887
+ 'production'`) needs a Worldpay merchant that payment-core pins for its
1888
+ paypageId at the merchant origin, and `merchant` must be that pin's name, the
1889
+ one the approval shows. The API refuses another name with
1890
+ `merchant_profile_mismatch` and a paypageId with no pin with
1891
+ `unsupported_checkout`, before any preparation exists; when this release pins
1892
+ the paypageId too, the SDK refuses another name first, before any network call.
1893
+ Agentcard has pinned no production merchant yet, so the API refuses every live
1894
+ eProtect checkout today; the pre-live host needs no pin. An approved answer that is not a
1895
+ registration for the paused request raises `PaymentOutcomeUnknownError` with
1896
+ reason `eprotect_registration_unconfirmed`: the device may have registered the
1897
+ card, so reconcile the merchant before trying again.
1898
+
1899
+ The SDK covers only Worldpay's own eProtect hosts, and does not pause a
1900
+ registration a merchant sends to an eProtect domain it names itself. The SDK
1901
+ does not support a merchant timeout of 5 seconds or less. The pre-live host
1902
+ receives only Worldpay's documented test cards. Autopilot never pays eProtect.
1903
+ The matching API, Vault and relay releases are required.
1904
+
1905
+ ### Pay a WSPay Components checkout
1906
+
1907
+ Hotel booking pages such as Valamar's take cards in WSPay Components, WSPay's
1908
+ build of Monri Components. The card field is WSPay's own iframe on
1909
+ `proxy.wspay.biz` (`proxy-test.wspay.biz` in test), and it sends the card in one
1910
+ request from that frame: `POST /v2/payment/{client_secret}/confirm`. The SDK
1911
+ pauses that request, the cardholder's device sends the real card to WSPay, and
1912
+ the frame receives WSPay's answer.
1913
+
1914
+ ```ts
1915
+ await vault.syncRegistry(); // the WSPay recognizer is never in the built-in list
1916
+ const checkout = await attachToPlaywright(page, {
1917
+ vault, user: 'your-user-id', merchant: 'Example Hotel', amount: '412.30', currency: 'EUR',
1918
+ });
1919
+ ```
1920
+
1921
+ - **Dark launch.** `syncRegistry()` declares the `wspay_confirm` capability,
1922
+ and the API serves the WSPay recognizer only to an org Agentcard has enabled
1923
+ for WSPay. Until then your SDK leaves every WSPay request as it did before
1924
+ this release: none is paused or refused. If Agentcard turns WSPay off for
1925
+ your org after a sync, the next create answers `403 processor_unavailable`
1926
+ and nothing is charged. `proxy-test.wspay.biz` takes only a test-mode client
1927
+ and `proxy.wspay.biz` only a live one.
1928
+ - **Pass the amount.** The confirm names no amount: WSPay fixed it when the
1929
+ merchant's server created the payment. The `amount` and `currency` you pass
1930
+ are what the approval shows. When WSPay approves at once, its answer names
1931
+ the amount and the API compares the two: a different amount reads
1932
+ `amountVerified: false` and the controller holds with reason
1933
+ `charged_amount_mismatch`. A 3-D Secure step names no amount, so nothing is
1934
+ compared on that path (below). Without `amount` and `currency` the confirm
1935
+ fails with `AmountRequiredError` before anything is created.
1936
+ - **What pauses.** Only the frame's confirm of a new card for a one-time
1937
+ payment, with the frame's own headers and body. A saved card (`saved_card`),
1938
+ a card tokenized earlier (`temp_card_id`), a card-shaped object the page
1939
+ built into the transaction, or any other request on that URL is aborted with
1940
+ a `blocked` event, detail `unsupported_wspay_payment_method`. Nothing is
1941
+ created, nobody is prompted, the controller's state does not change, and the
1942
+ page's next confirm is judged afresh. The frame's other requests (`/show`,
1943
+ `/status`, `/pan-info`) continue untouched.
1944
+ - **Keep on file.** A confirm with `tokenize_pan: true` asks WSPay to keep the
1945
+ card so the merchant can charge it later without another approval. Only the
1946
+ cardholder can agree to that and no approval asks them yet, so such a
1947
+ confirm is aborted with a `blocked` event, detail
1948
+ `wspay_keep_on_file_unsupported`, and nothing is created. Valamar sets it on
1949
+ every booking, so a Valamar checkout is refused this way until the approval
1950
+ asks the cardholder for that consent.
1951
+ - **3-D Secure.** When WSPay answers with its 3-D Secure step, the frame runs
1952
+ the bank's challenge in your browser, and nothing is charged until the
1953
+ cardholder completes it there. The controller reads `requires_user_action`
1954
+ with reason `3ds`, and `onUserAction` receives the same reason: show the
1955
+ challenge to the cardholder, then `reconcile()` the merchant's order. After
1956
+ the challenge WSPay charges the amount the merchant's server set, and no
1957
+ answer Agentcard receives names it: `amountVerified` stays `null`. Read the
1958
+ charged amount from the merchant's order and compare it with the one the
1959
+ cardholder approved before you report the purchase complete.
1960
+ - **The client_secret** in the confirm URL is a per-payment secret. `redactUrl`
1961
+ and every event report the URL as
1962
+ `https://proxy.wspay.biz/v2/payment/{client_secret}/confirm`.
1963
+ - Autopilot never pays a WSPay checkout; the cardholder approves each one on
1964
+ their device. Monri Components on `ipg.monri.com` and the WSPay Form hosted
1965
+ page are not covered.
1966
+
1967
+ ### Verifone merchants that encrypt the card in the page
1968
+
1969
+ Some merchants (The Container Store) have Verifone's verifone.js encrypt the card in the page, under the merchant's own Verifone key, and post the result to the merchant's own server. As for the Adyen merchants above, the card request goes to no processor host, so Agentcard reviews each such merchant as a Verifone profile: its checkout page, its card request and the key verifone.js must encrypt the card under. Verifone ships off. `syncRegistry()` declares `verifone_card_capture`, and the API serves the Verifone profiles only to a build that declares it, for a company Agentcard turned Verifone on for. Until a sync serves one, the adapters pause no request at a Verifone merchant, as before. A served profile arms every request to the merchant's origin in both adapters:
1970
+
1971
+ - A request that carries no verifone.js blob (a page, a basket call, a preflight, a request with no body) continues as before. When the browser holds a body it did not hand over, such as a blob or a file, `attachToCdp` aborts the request as one the profile cannot pay, except on a `GET`, `HEAD` or `OPTIONS`. `attachToPlaywright` cannot tell such a body from no body, so it reads one as no body.
1972
+ - A profile in `observe` never pays. The adapters abort a request that carries a blob before it reaches the merchant, and report `verifone_profile_observed` with the profile, the URL (the origin and a reviewed path template, never a query), the verdict and `keyPathSha256`, then `unsupported_checkout`. The controller reads `unsupported` with reason `merchant_profile_observe`. `containerstore_checkout` ships in `observe`: Agentcard has reviewed neither its card request nor its key.
1973
+ - The adapters abort a blob request the profile cannot pay (another path, a second card, saving the card, a gift card, a key Agentcard did not pin) and report `unsupported_checkout` with the reason.
1974
+ - The profile's reviewed card request, once Agentcard enables the profile for your client and this build, pays only through a preparation for that profile. Without one, the adapters abort it before any pause, prompt or authorization, as for an Adyen merchant.
1975
+
1976
+ ```ts
1977
+ await controller.prepare({ psp: 'verifone', environment: 'production', merchantProfile: 'containerstore_checkout' });
1978
+ ```
1979
+
1980
+ The environment is the one of the key the profile pins. The cardholder approves a payment to the merchant the profile names, whatever label the agent passed. After approval, click Pay once. The SDK sends the API the profile id with the paused request (`verifone: { profile }`, never Autopilot). The API checks the body against the profile, holds the agent's amount to the one the body names, and keeps only the SHA-256 of the body and of the page's blob. The cardholder's device encrypts the approved card the way verifone.js does, under the key Agentcard pinned for that merchant, never a key from the page. The approval comes back as a `cse` replay with `kind: 'verifone'`, and the request continues from your browser with the device's blob in place of the page's, but only if the live body still hashes to the body the API checked (`bodySha256`). The blob pays the merchant for 15 minutes, so the API serves it only in the moments after the approval. After hand-off, the merchant's server charges the card and answers its own page, and the outcome rules above for Adyen merchants apply unchanged.
1981
+
1982
+ A raw CDP runtime arms `vault.verifoneProfileUrlPatterns()` after `syncRegistry()`, reads `vault.verifoneProfileOf(url)` and `classifyVerifoneProfileRequest(profile, url, method, body)` for each paused request (`body` is `''` for a request with no body), and writes a Verifone approval with `substituteVerifoneBody({ url, method, body }, replay)`. Never continue a body that carries a verifone.js blob to a profile's origin unless `authorize()` paid it.
1983
+
935
1984
  ### Unsupported endpoints
936
1985
 
937
1986
  `paymentEndpoints` is an explicit list supplied by the integrator after observing