@agent-cards/sdk 0.21.0 → 0.23.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 (113) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/PREFLIGHT.md +32 -0
  3. package/README.md +1177 -23
  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 +302 -56
  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-facts.d.ts +96 -0
  18. package/dist/card-endpoint-facts.js +275 -0
  19. package/dist/card-endpoint-page.d.ts +54 -0
  20. package/dist/card-endpoint-page.js +69 -0
  21. package/dist/card-endpoint.d.ts +298 -0
  22. package/dist/card-endpoint.js +438 -0
  23. package/dist/card-fields.generated.d.ts +5 -3
  24. package/dist/card-fields.generated.js +284 -7
  25. package/dist/cdp.d.ts +5 -1
  26. package/dist/cdp.js +1155 -141
  27. package/dist/client.d.ts +456 -20
  28. package/dist/client.js +1072 -104
  29. package/dist/creditguard.generated.d.ts +79 -0
  30. package/dist/creditguard.generated.js +662 -0
  31. package/dist/cse-body.js +11 -1
  32. package/dist/cybersource-flex.d.ts +30 -0
  33. package/dist/cybersource-flex.generated.d.ts +27 -1
  34. package/dist/cybersource-flex.generated.js +42 -2
  35. package/dist/cybersource-flex.js +46 -0
  36. package/dist/datatrans.d.ts +96 -0
  37. package/dist/datatrans.generated.d.ts +64 -0
  38. package/dist/datatrans.generated.js +426 -0
  39. package/dist/datatrans.js +178 -0
  40. package/dist/ekashu.d.ts +35 -0
  41. package/dist/ekashu.js +55 -0
  42. package/dist/elavon-converge.d.ts +37 -0
  43. package/dist/elavon-converge.generated.d.ts +49 -0
  44. package/dist/elavon-converge.generated.js +439 -0
  45. package/dist/elavon-converge.js +84 -0
  46. package/dist/eprotect.d.ts +152 -0
  47. package/dist/eprotect.generated.d.ts +77 -0
  48. package/dist/eprotect.generated.js +498 -0
  49. package/dist/eprotect.js +222 -0
  50. package/dist/hosted-form.d.ts +3 -2
  51. package/dist/hosted-form.js +3 -2
  52. package/dist/index.d.ts +17 -3
  53. package/dist/index.js +6 -1
  54. package/dist/juspay-gate.d.ts +82 -0
  55. package/dist/juspay-gate.js +126 -0
  56. package/dist/juspay.generated.d.ts +89 -0
  57. package/dist/juspay.generated.js +705 -0
  58. package/dist/lifecycle.d.ts +60 -6
  59. package/dist/lifecycle.js +122 -8
  60. package/dist/merchant-card-endpoints.generated.d.ts +122 -0
  61. package/dist/merchant-card-endpoints.generated.js +4261 -0
  62. package/dist/merchant-hosted.d.ts +4 -1
  63. package/dist/merchant-hosted.js +5 -2
  64. package/dist/merchant-session-probe.d.ts +48 -0
  65. package/dist/merchant-session-probe.js +53 -0
  66. package/dist/merchant-session.generated.d.ts +69 -0
  67. package/dist/merchant-session.generated.js +919 -0
  68. package/dist/payrails.d.ts +133 -0
  69. package/dist/payrails.generated.d.ts +73 -0
  70. package/dist/payrails.generated.js +596 -0
  71. package/dist/payrails.js +305 -0
  72. package/dist/pelecard.d.ts +32 -0
  73. package/dist/pelecard.generated.d.ts +32 -0
  74. package/dist/pelecard.generated.js +283 -0
  75. package/dist/pelecard.js +42 -0
  76. package/dist/pre-claim.d.ts +81 -2
  77. package/dist/pre-claim.js +1023 -5
  78. package/dist/preflight-catalog.json +2267 -288
  79. package/dist/preflight-schemas.json +183 -6
  80. package/dist/preflight.generated.d.ts +1 -1
  81. package/dist/preflight.generated.js +243 -4
  82. package/dist/preparation.d.ts +12 -3
  83. package/dist/preparation.js +63 -12
  84. package/dist/prepared-processor.d.ts +84 -5
  85. package/dist/prepared-processor.js +147 -6
  86. package/dist/primer.d.ts +53 -0
  87. package/dist/primer.generated.d.ts +38 -0
  88. package/dist/primer.generated.js +624 -0
  89. package/dist/primer.js +82 -0
  90. package/dist/registry.d.ts +100 -8
  91. package/dist/registry.js +85 -11
  92. package/dist/shiji-sts.d.ts +144 -0
  93. package/dist/shiji-sts.generated.d.ts +72 -0
  94. package/dist/shiji-sts.generated.js +913 -0
  95. package/dist/shiji-sts.js +222 -0
  96. package/dist/stripe-checkout.generated.d.ts +1 -1
  97. package/dist/stripe-checkout.generated.js +764 -9
  98. package/dist/stripe-setup.d.ts +16 -0
  99. package/dist/stripe-setup.js +61 -0
  100. package/dist/substitutions.generated.js +187 -29
  101. package/dist/verifone.d.ts +68 -0
  102. package/dist/verifone.generated.d.ts +51 -0
  103. package/dist/verifone.generated.js +1413 -0
  104. package/dist/verifone.js +119 -0
  105. package/dist/voice-types.d.ts +27 -0
  106. package/dist/voice.d.ts +6 -1
  107. package/dist/voice.js +6 -1
  108. package/dist/wspay.d.ts +41 -0
  109. package/dist/wspay.generated.d.ts +41 -0
  110. package/dist/wspay.generated.js +370 -0
  111. package/dist/wspay.js +65 -0
  112. package/examples/preflight/kernel-native/inventory.json +1 -1
  113. 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, Delta, Hilton, IHG, American Airlines, EconomyBookings, The Verb Hotel, RunSignUp, Mindbody, LookFantastic, Lexicon (FastSpring's popup checkout), and Hyatt, LifeMiles, Southwest Airlines and United Airlines, whose page encrypts the card under the merchant's own key, and the card forms Athens-Clarke County's MyRec page, Los Angeles and San Francisco on Conduent eTIMS, the D-EDGE card frame and NIC Common Checkout (NJMCdirect and other state agencies) submit as a page load | 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, and a request to the same URL with no card in it continues untouched. 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,33 @@ 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,ekashu_hosted_payment_page`
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`. NMI's hosted
302
+ payment page (`ekashu_hosted_payment_page`) is served on the same terms: this
303
+ build pauses the merchant's entry form to `live.ekashu.com` for the
304
+ cardholder's device and fails every other post your browser makes to NMI, so
305
+ NMI never sees the card your agent typed.
255
306
 
256
307
  ## Modes
257
308
 
258
309
  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.
310
+ carries the mode it was handled in. The adapters do the right thing for each
311
+ mode; the difference matters if you drive `authorize()` yourself.
261
312
  `ReplayResponse` is a union, so branch on `mode`.
262
313
 
263
314
  - **`token`** (every processor but Adyen and Fiserv). The cardholder's device calls the
@@ -305,8 +356,9 @@ three; the difference matters if you drive `authorize()` yourself.
305
356
  paused capture or the envelope names another key than the capture does. The
306
357
  API serves a Fiserv envelope only in the moments after the approval; a later
307
358
  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
359
+ - **`hosted_form`** (Tranzila, Authorize.Net DPM and SIM). The processor's
360
+ hosted card form, or the merchant's own card form that posts to the
361
+ processor, submits the card as a form post, so the paused request is a navigation
310
362
  (the adapters arm `Fetch.enable` with no resource-type filter and attach the
311
363
  processor's iframe, which is how a Document request on `direct.tranzila.com`
312
364
  gets paused at all). The cardholder's device rebuilds that form with the
@@ -336,15 +388,170 @@ three; the difference matters if you drive `authorize()` yourself.
336
388
  without a round trip, and once the prior authorization is declined or
337
389
  expired the same form is a new question. Confirm the order with the
338
390
  merchant, which learns the outcome from the processor.
391
+ - **`device_encrypt`** (Payrails). The page encrypts the card under a
392
+ per-session key that the paused request does not carry: it came in the
393
+ Payrails init payload the merchant's backend handed the page. The adapters
394
+ read that payload from the page's own XHR and fetch answers and send its key
395
+ with the create as `request.payrails.encryption_public_key`; a caller that
396
+ runs its own interception passes it the same way, and a Payrails save
397
+ without it throws before anything is created. The cardholder's device
398
+ encrypts the real card under that key and sends the save to Payrails
399
+ itself, and `authorize()` resolves with `{ mode: 'device_encrypt', status,
400
+ headers, body }`: Payrails' 201, rebuilt from an allow-list, to fulfill the
401
+ paused request with as in `token`. The ciphertext the device made never
402
+ reaches your runtime. Any other answer throws `PaymentOutcomeUnknownError`
403
+ (`payrails_answer_unconfirmed`), and autopilot never runs this mode. See
404
+ [Pay a Payrails checkout](#pay-a-payrails-checkout).
405
+
406
+ Authorize.Net's Direct Post Method form (the merchant's own card form) and
407
+ its SIM hosted form both post to `/gateway/transact.dll`, with the card in
408
+ `x_card_num`, `x_exp_date` and `x_card_code` beside the merchant's signed
409
+ fingerprint. Authorize.Net reads field names without regard to case, and so
410
+ does the SDK: `X_Card_Num` is a card request too. The same URL without a
411
+ card continues untouched: SIM's first step (`x_show_form=PAYMENT_FORM`,
412
+ which loads the hosted form), an eCheck and Visa Checkout. The endpoint is
413
+ the bare URL: a form whose action carries a query string or a fragment is
414
+ not recognized and continues untouched, exactly as the API refuses it. A
415
+ fingerprint that Authorize.Net would find stale (more than 40 minutes old
416
+ at the create, or ahead of the API's clock) is refused for that form only:
417
+ the page reloads with a fresh fingerprint and pays. Authorize.Net spends a
418
+ fingerprint once, so never resubmit a form the cardholder's device sent.
419
+
420
+ - **`card_endpoint`** (a reviewed merchant's own card endpoint). Some
421
+ merchants post the typed card from their own checkout page straight to
422
+ their own server, with no processor in between. Agentcard reviews each one
423
+ and pins a profile in this SDK's copy of the payment core: the checkout
424
+ page's origin, the exact origin, path and query of the card request, the
425
+ session headers and cookies it needs, and the answer headers that may come
426
+ back. The adapters arm every profile's endpoint (`cardEndpointPatterns()`
427
+ lists the `Fetch.enable` patterns) and pause a POST to exactly a profile's
428
+ card request URL when it carries a card: anything at the profile's card
429
+ number field, or a Luhn-valid number of 12 to 19 digits anywhere in the
430
+ body, however it is written. A body the adapter cannot read counts as
431
+ carrying one. A GET, a CORS preflight, a look-alike URL, or a POST to the
432
+ same URL with no card in it (an IHG stay paid with points, a LookFantastic
433
+ order whose card a processor's own fields tokenized, a saved card, a room
434
+ another SynXis hotel guarantees with a token) continues untouched. While this
435
+ build's copy of a profile does not take payments (every profile is in
436
+ observe today) a card request is refused as `unsupported_checkout` before
437
+ any authorization exists. When the body is the request the profile
438
+ reviewed, the controller reads `unsupported` with reason
439
+ `merchant_card_endpoint_not_enabled` and the event names the profile; a
440
+ card in any other body reads reason `unrecognized_payment_endpoint`, and
441
+ the event names only the endpoint origin. For a
442
+ profile that takes payments, the adapter captures the request's session:
443
+ the profile's own header names from the paused request, the endpoint's
444
+ cookie jar for a cookie profile (`Network.getCookies` or
445
+ `context.cookies()`, since the paused headers never carry it), and a
446
+ `Referer` only when it is a page on the origin the request is sent from: the
447
+ merchant's own card vault frame when the profile names one (EconomyBookings),
448
+ else the checkout page. It sends them with
449
+ the paused body, which holds only the dummy card, in the create's
450
+ `merchant_card_endpoint` member, never in `request`, and checks them first
451
+ with the same rules the API and the device run. The cardholder's device
452
+ sends the card request to the merchant itself over Agentcard's checkout
453
+ relay and reports the merchant's answer; `authorize()` resolves with
454
+ `{ mode: 'card_endpoint', profile, status, headers, setCookies, body,
455
+ withheld }`. The adapters write each `Set-Cookie` into your browser
456
+ host-only on the endpoint host and Secure, so none of them travels over
457
+ plain http. A `Set-Cookie` whose `Domain` names a parent of the endpoint
458
+ host that your browser already holds a cookie of that name on is written
459
+ there instead, so it replaces that cookie rather than sitting beside it; one
460
+ whose `Domain` the endpoint host is not inside is dropped, as a browser
461
+ drops it. Then the adapters
462
+ fulfill the paused request with the merchant's status, the profile's
463
+ answer headers, the CORS headers a cross-origin page needs, and the body.
464
+ When the device withheld the answer (it carried the card, or a compressed
465
+ stream the device cannot read), the paused request gets a fixed `502` JSON
466
+ error with code `agentcard_answer_withheld`, and the controller reads
467
+ `outcome_unknown` with reason `merchant_answer_withheld`: the merchant has
468
+ the card, so check the order. Every payment needs the cardholder's
469
+ approval (no Autopilot, no `prepare()`), and the `amount` you pass is the
470
+ only amount authority, so an attachment without one is refused. The
471
+ approval never outlives its request: a card request the page gives up on
472
+ retires the authorization. After the hand-off the attachment holds, so the
473
+ page's retry of the card request, or a step-up that sends the card again,
474
+ is refused until you reconcile the order.
475
+
476
+ Lexicon sells through FastSpring's popup checkout, which needs four more
477
+ rules. The card is typed into FastSpring's frame on the store's own host
478
+ inside Lexicon's page, so the page a card request pauses on is that frame's
479
+ document (the frame tree in CDP, `request.frame()` in Playwright). FastSpring
480
+ posts PayPal and wallet payments to the same URL: a body whose `paymentType`
481
+ names another method and that carries no card continues untouched, and the
482
+ adapters emit `card_endpoint_passed`; one that names another method but
483
+ carries a card is held like any card request. FastSpring's EBANX step,
484
+ which reads the typed card back and posts it to `/3ds/return-ebanx`, is
485
+ aborted in every status
486
+ (`card_endpoint_refused` with reason `card_resend`, the controller reads
487
+ `unsupported` with reason `merchant_card_endpoint_resend_refused`); the
488
+ device withholds the answer that would start it, which the paused request
489
+ receives as a fixed `502` with code `agentcard_step_refused`. And the SDK
490
+ records FastSpring's own record of the order, read-only, from the popup
491
+ document and each JSON answer its page reads the order from, a quantity or
492
+ add-on change included. When the card request pauses it sends the newest one
493
+ with the create, cut to the total with tax, the currency, test mode and the
494
+ renewal marks; a total or currency other than your integer `amount` is an
495
+ `AmountMismatchError` against the merchant's total, and a renewing order or
496
+ a checkout in FastSpring's test mode is refused as `facts_refused`, both
497
+ before any authorization. A request the popup sends on the same checkout
498
+ after that record, other than one the SDK reads the order from or
499
+ FastSpring's customer recognition, leaves the order unknown: the payment is
500
+ refused as `facts_unavailable`, and a reload of the popup reads it again. The
501
+ SDK keeps 32 of these requests. A checkout that sends more is refused the
502
+ same way when one the SDK let go could be newer than the record it read, and
503
+ for the rest of that checkout session once one went while still loading. Card
504
+ endpoint events name the
505
+ `profile` and the `endpoint` origin, never the request's URL, body or
506
+ session: the URL carries the merchant's cart and booking ids.
507
+
508
+ Some of these merchants' pages submit the card form as a page load rather
509
+ than a fetch: the profile's `navigation` is `top`, or `frame` for a form
510
+ inside an iframe, which is then judged by the iframe's own document. The
511
+ same URL takes the page's other posts, so a post whose card fields are
512
+ empty and that holds no card number (a Cancel button, another payment
513
+ method) continues untouched in any status, and a card post a script sends
514
+ is aborted, since no page waits on it. The reviewed page load pauses, its
515
+ session carries only the cookie names the profile lists, and the
516
+ merchant's answer comes back as a page load: its page, or a 301, 302 or
517
+ 303 your browser follows in its own session with the merchant's cookies
518
+ written first. A 307 or 308 would make the browser post the typed form
519
+ again, so it is refused. Every stopped page load (a profile in observe, a
520
+ post that is not the reviewed form, a declined approval, an approval
521
+ already outstanding, a refused redirect, a withheld answer) is answered
522
+ with a small HTML page instead of an aborted load. Its
523
+ `<meta name="agentcard-checkout">`, its `x-agentcard-checkout` header and
524
+ its JSON block (`#agentcard-checkout`) say one of three outcomes.
525
+ `stopped_before_send`: nothing reached the merchant, and no other payment
526
+ for this checkout is waiting. `sent_answer_not_shown`: the card reached the
527
+ merchant, so check the payment with the merchant before paying again.
528
+ `payment_in_progress`: a payment for this checkout is waiting for approval
529
+ or may have reached the merchant, so check that payment and never send the
530
+ form again. The page reads `payment_in_progress` while an approval is
531
+ outstanding (the cardholder's device sends the card before it approves),
532
+ after a payment this attachment handed over or cannot account for, and when
533
+ the API refuses the create with `409 merchant_checkout_in_use`, as it does
534
+ on every later post of that attachment. A local deadline whose
535
+ authorization the API confirms it retired before any processor request
536
+ reads `stopped_before_send`: the `PaymentOutcomeUnknownError` then carries
537
+ `processorRequestStarted: false`.
339
538
 
340
539
  `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.
540
+ (`token,cse,hosted_form,device_encrypt`), so a processor whose flow this build
541
+ cannot finish is never paused; the API serves `hosted_form` and `device_encrypt`
542
+ entries only to callers that ask.
543
+ `syncRegistry()` also sends `SUPPORTED_CAPABILITIES`
544
+ (`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`),
545
+ the dark-launch half of the same negotiation: the API serves a processor gated
546
+ behind a capability only to a build that declares it, for a company Agentcard
547
+ turned that processor on for, and the built-in list never holds one, so the SDK
548
+ pauses it only after a sync. A registry entry naming a capability this build
549
+ does not declare throws `UnsupportedCapabilityError` before creation. A runtime
550
+ that calls `GET /v2/checkout/recognizers` itself without that parameter, such as
551
+ a native provider integration, never receives the Fiserv, GMO, CyberSource Flex,
552
+ Juspay, Datatrans, Authorize.Net DPM, Pelecard, Primer, Payrails, Worldpay eProtect,
553
+ WSPay, Shiji STS or Amadeus recognizer. See "Prepare a Worldpay eProtect checkout (dark launch)" and "Pay a
554
+ WSPay Components checkout" below.
348
555
  A registry mode this SDK cannot finish throws `UnsupportedModeError` before
349
556
  creation. An approval returned in an unexpected mode has an unknown outcome
350
557
  and holds the attachment for reconciliation. The `authorized` event's detail names
@@ -355,6 +562,71 @@ detail names the `authorizationId`, `submittedAt` and `outcome:
355
562
  one. `amountAuthority` on every replay is `stripe_payment_intent`,
356
563
  `hosted_form_sum` (the form's own amount) or `display_only`.
357
564
 
565
+ ### Shiji STS (MyCheck Wallet V3)
566
+
567
+ La Quinta by Wyndham takes the card through MyCheck's wallet, whose Shiji proxy
568
+ (`sts.proxy.v2.1.js`) encrypts the PAN and the `MM/YY` expiry with RSA PKCS#1
569
+ v1.5 under a key Shiji hardcodes and posts
570
+ `{accessToken, pan, expires, keySequence}` to
571
+ `/v1/token/getTokenId` on `iframe.vault.eu.prod.shijipayment.com` (sandbox:
572
+ `iframe.vault.eu.uat.shijipayment.com`), then the CVV the same way to
573
+ `/v1/token/getCvvId`. The processor ships dark: `syncRegistry()` declares
574
+ `shiji_sts_pinned_key` (`SUPPORTED_CAPABILITIES`), and the API serves the
575
+ recognizer only to an org Agentcard has enabled for it. Every other org's SDK
576
+ recognizes neither URL, and the page's requests go through untouched, as
577
+ before.
578
+
579
+ For an enabled org, both adapters judge each POST to those two URLs before
580
+ anything pauses:
581
+
582
+ - A `getTokenId` in the proxy's own shape is the one prompt. The cardholder's
583
+ device encrypts the real card under the key Agentcard pins (never a key from
584
+ the page), `authorize()` resolves with `substitutions: { encoding: 'json',
585
+ at: '', fields: { pan, expires, cvv } }`, and the adapter writes `pan` and
586
+ `expires` over the page's own at the body root and continues the request
587
+ from the same browser. The `authorized` event names `fields: ['pan',
588
+ 'expires']`. The ciphertext goes only into the request the approval was
589
+ created from, byte for byte. If the page gives up on that request while the
590
+ cardholder decides, the approval is cancelled (`outcome_unknown`, reason
591
+ `merchant_request_aborted`), as for a hosted form: no later `getTokenId`
592
+ is answered from it. Shiji uses one key for every merchant, so a request of
593
+ the same shape could name another merchant's `accessToken` and
594
+ `keySequence`, and the proxy's own request never times out.
595
+ - While Agentcard cannot ask the cardholder for consent to a hotel keeping the
596
+ card, every such `getTokenId` is refused before any create or prompt:
597
+ `unsupported_checkout` with reason `shiji_sts_keep_on_file`, and the
598
+ attachment reads `unsupported`. Nothing reaches Shiji.
599
+ - A `getCvvId` never prompts. It continues with the device's CVV only when it
600
+ follows the `getTokenId` this attachment continued, from the same frame,
601
+ with the same `accessToken`, under a `keySequence` the API reviewed as the
602
+ booking page's own `getCvvId` key and served with the approval, within 120
603
+ seconds, and only once (a `companion_continued` event). Every other
604
+ `getCvvId` is refused as `shiji_sts_companion_unbound`,
605
+ `shiji_sts_companion_expired` or `shiji_sts_companion_used`, so the agent's
606
+ placeholder CVV never rides beside a real card and the real one goes to no
607
+ unreviewed key. An approval served with no reviewed `getCvvId` key resolves
608
+ as `PaymentOutcomeUnknownError` (`cse_substitutions_malformed`) and nothing
609
+ continues. The device's CVV ciphertext is dropped from memory as soon as no
610
+ `getCvvId` can use it: when the attachment is cancelled, the page, the frame
611
+ or the target that sent the `getTokenId` goes away, or that frame loads a new
612
+ document (a reload included), which never obtained the approval. The same
613
+ document changing its path (`history.pushState`), its hash, or following a
614
+ link answered 204 keeps it, even while a navigation it later cancels is
615
+ pending. CDP names the new document (`Page.frameNavigated`); Playwright does
616
+ not, so `attachToPlaywright` reads the frame's `performance.timeOrigin` when
617
+ the `getTokenId` pauses and again for each `getCvvId`, and continues one only
618
+ from the same document.
619
+ - Anything else on those URLs (another body shape, a query string, a field
620
+ encrypted under a larger key) is refused as `unsupported_shiji_sts_request`
621
+ or `shiji_sts_foreign_key`. A body the adapter cannot read is aborted alone
622
+ (`blocked`, `unreadable_shiji_sts_request`).
623
+
624
+ The request names no amount, so pass the booking total as `amount` and
625
+ `currency`: the API refuses a Shiji create without it (`amount_required`).
626
+ Autopilot never pays Shiji. A Shiji set is served once; an approval whose set
627
+ is gone resolves as `PaymentOutcomeUnknownError` with reason
628
+ `cse_substitutions_expired`.
629
+
358
630
  ## Errors worth handling
359
631
 
360
632
  - `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 +679,13 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
407
679
  reporting to the merchant you named (a reusable token in or out, a sale
408
680
  mode that is not a sale, a callback URL off the merchant's host: name the
409
681
  merchant by its hostname when the form carries callback URLs); all three
410
- are terminal for that page. `duplicate_submission` (409,
682
+ are terminal for that page. An Authorize.Net DPM or SIM form adds more
683
+ reasons (`duplicate_field`, `not_a_card_payment`, `relay_response_off`,
684
+ `form_step`, `special_flow`, `merchant_credential`,
685
+ `test_request_on_live_host`, `malformed_field`), terminal the same way,
686
+ and `stale_fingerprint`, which is not: `CheckoutApiError.isStaleFingerprint`
687
+ is true, the adapters quiet the page for `approvalCooldownMs`, and the form
688
+ the page renders after a reload is judged afresh. `duplicate_submission` (409,
411
689
  with `prior_authorization_id` and `prior_status`) means this exact
412
690
  submission already has, or already had, its prompt: the adapters quiet the
413
691
  page's re-posts for `approvalCooldownMs` (no second notification for one
@@ -429,20 +707,52 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
429
707
  page changed, throws `CheckoutPreparationError` with the API's code as its
430
708
  `reason`.
431
709
  - `AmountRequiredError` (`code: 'amount_required'`): the processor's request
432
- names no amount (GMO) and you passed none, so there is nothing for the
710
+ names no amount (GMO, Pelecard, WSPay) and you passed none, so there is nothing for the
433
711
  cardholder to approve. Thrown before anything is created; terminal for that
434
712
  page. Pass `amount` and `currency`.
713
+ - Datatrans refusals at create, all terminal for that page: `CheckoutApiError`
714
+ with `code` `processor_unavailable` (403, Datatrans is not enabled for your
715
+ org), `invalid_datatrans_request`, `datatrans_card_on_file_refused`,
716
+ `datatrans_merchant_unregistered` (the post names a merchant account
717
+ Agentcard has not reviewed), `amount_required` (400; you passed no amount
718
+ above zero and the page total could not be read), or `sandbox_host_on_live_row` and
719
+ `live_host_on_sandbox_row` (400; `pay.sandbox.datatrans.com` takes only a
720
+ test-mode client and `pay.datatrans.com` only a live one). The adapters
721
+ abort the posts behind `invalid_datatrans_request` and, while the served
722
+ card-on-file policy refuses card on file,
723
+ `datatrans_card_on_file_refused` themselves, before any create, and those
724
+ two do not end interception for the page.
435
725
  - `CardEncryptedError`: this processor encrypts the card in-page and its
436
726
  registry entry does not (yet) say the vault can produce that ciphertext;
437
727
  route the purchase to an Agentcard-issued card instead.
438
728
  - `UnsupportedModeError`: the registry requests a mode this SDK cannot finish
439
729
  before an authorization exists. Upgrade. An unexpected approved mode instead
440
730
  raises `PaymentOutcomeUnknownError` and requires reconciliation.
731
+ - `CheckoutPreparationError`: `reason` names why a preparation stopped. For
732
+ CreditGuard, the SDK refuses before any request with `payment_page_required`,
733
+ `unsupported_payment_page`, `payment_page_environment_mismatch` (the page's
734
+ host is not of the environment you named), `payment_page_unsupported` (a
735
+ page URL for another processor) or `merchant_origin_invalid`; the API's
736
+ refusals come through as `creditguard_transaction_refused` (CreditGuard's
737
+ reason in `detail`), `creditguard_transaction_unavailable`,
738
+ `amount_mismatch`, `processor_unavailable`, `sandbox_host_on_live_row` and
739
+ `live_host_on_sandbox_row`. Nothing was created and nothing was charged.
441
740
  - `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`.
741
+ pays only through a prepared checkout (Fiserv, CreditGuard, Worldpay eProtect)
742
+ passed no preparation. Nothing was created and nothing was charged; call
743
+ `prepareCheckout()` first. The adapters refuse such a request before it
744
+ pauses, with the controller reason `preparation_required`. For Worldpay
745
+ eProtect the refusal is terminal for the attachment: attach again and call
746
+ `prepare()` before the first Pay action.
747
+ - `PaymentOutcomeUnknownError` with reason `creditguard_response_unconfirmed`:
748
+ the approved CreditGuard answer is not an approval of the paused
749
+ transaction, so the page never reads it. Reconcile the order with the
750
+ merchant before another attempt.
751
+ - `PaymentOutcomeUnknownError` with reason `response_expired`: the API no
752
+ longer serves the approved answer. A CreditGuard answer carries
753
+ CreditGuard's card token in its landing url, so the API serves it only for
754
+ 15 minutes after the approval, the default `timeoutMs`. Reconcile the
755
+ order with the merchant before another attempt.
446
756
  - `UnsupportedCapabilityError`: the registry entry is gated behind a
447
757
  dark-launch capability this build does not declare. Only a hand-built
448
758
  registry can hold one. Upgrade.
@@ -454,12 +764,93 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
454
764
  `microform_unsupported` or `invalid_cybersource_request` (400). An approved
455
765
  Flex authorization whose answer is not a transient token raises
456
766
  `PaymentOutcomeUnknownError` with reason `cybersource_flex_token_unconfirmed`.
767
+ - Juspay refusals at create, all terminal for that page: `CheckoutApiError`
768
+ with `code` `processor_unavailable` (403, not enabled for your org),
769
+ `invalid_juspay_request` (400, with a `reason`: the paused `/txns` is not the
770
+ reviewed payment) or `sandbox_host_on_live_row` (400). An approved Juspay
771
+ authorization whose answer is outside the allowlist raises
772
+ `PaymentOutcomeUnknownError` with reason `juspay_response_unconfirmed`, and a
773
+ `ProcessorRefusedError` whose `pspErrorCode` starts with `juspay_tokenize_`
774
+ means the cardholder's device never sent the payment and nothing was charged.
775
+ - The SDK refuses these Juspay requests in the browser, before the API creates
776
+ an authorization and before anything reaches Juspay. It throws nothing for
777
+ them: the attachment's state and its events carry the code.
778
+
779
+ | Code | Where it appears | What it means | What to do next |
780
+ |---|---|---|---|
781
+ | `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. |
782
+ | `juspay_placeholder_fingerprint` | a `blocked` event | The SDK refuses the page's lookup of the placeholder card. | Nothing: the page carries on without it. |
783
+ | `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. |
784
+ | `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. |
785
+
786
+ `juspay_request_unsupported` holds the attachment as `unsupported` until you
787
+ attach again. Each `blocked` code refuses only its own request, and the
788
+ attachment stays as it was. With `onEvent: console.log` on a Playwright
789
+ attachment, a payment with "save card" ticked logs these three events, and
790
+ `console.log(checkout.getState())` then prints the last object:
791
+
792
+ ```text
793
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
794
+ { type: 'checkout_prepared', detail: { processor: 'juspay' } }
795
+ {
796
+ type: 'unsupported_checkout',
797
+ detail: {
798
+ url: 'https://api.juspay.in/txns',
799
+ method: 'POST',
800
+ processor: 'juspay',
801
+ reason: 'save_to_locker'
802
+ }
803
+ }
804
+ {
805
+ status: 'unsupported',
806
+ authorizationId: null,
807
+ reason: 'juspay_request_unsupported'
808
+ }
809
+ ```
810
+
811
+ A page that sends the three requests the SDK refuses alone logs one `blocked`
812
+ event for each: the placeholder's fingerprint lookup, a `/txns` with a
813
+ stand-in token this attachment never issued, and a POST with no body.
814
+ `console.log(checkout.getState())` then prints the last object:
815
+
816
+ ```text
817
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
818
+ { type: 'blocked', detail: 'juspay_unknown_synthetic_token' }
819
+ { type: 'blocked', detail: 'juspay_request_body_unreadable' }
820
+ { status: 'idle', authorizationId: null }
821
+ ```
822
+ - WSPay refusals at create, all terminal for that page: `CheckoutApiError`
823
+ with `code` `processor_unavailable` (403, WSPay was turned off for your org
824
+ after your last `syncRegistry()`),
825
+ `invalid_wspay_request`, `wspay_keep_on_file_unsupported`, `amount_required`,
826
+ `sandbox_host_on_live_row` or `live_host_on_sandbox_row` (400;
827
+ `proxy-test.wspay.biz` takes only a test-mode client and `proxy.wspay.biz`
828
+ only a live one). The adapters abort the requests behind
829
+ `invalid_wspay_request` and `wspay_keep_on_file_unsupported` themselves, and
830
+ `authorize()` refuses a missing amount, all before any create.
457
831
  - `PlaceholderCardError`: `placeholderCard()` found no such stored card
458
832
  (`card_not_found`), no placeholder for its network (`brand_unsupported`), or
459
833
  no expiry on it (`expiry_unknown`). Nothing was created.
460
834
  - `SubstitutionError`: a `cse` approval could not be written into the paused
461
835
  body (the four encrypted fields were not there). The request is failed and
462
836
  the next one is judged afresh.
837
+ - `CardEndpointRefusedError`: a payment to a reviewed merchant's own card
838
+ endpoint was refused before anything was created or sent. `reason` is
839
+ `not_enabled` (this build's profile does not take payments),
840
+ `amount_required`, `approval_required` (the attachment asks for
841
+ Autopilot), `preparation_unsupported`, or `request_unsupported` (the
842
+ paused request is not the card request Agentcard reviewed; `detail` names
843
+ fields, never values), `facts_unavailable` (the merchant keeps its own
844
+ record of the order and this runtime read none for this checkout) or
845
+ `facts_refused` (that record says the order renews, or that the checkout is
846
+ in test mode). Every reason but `request_unsupported`, `facts_unavailable`
847
+ and `facts_refused` holds the attachment. The adapters also emit `card_endpoint_refused` with the
848
+ profile, the endpoint origin and the reason.
849
+ - `PageAmountRequiredError` (`code: 'page_amount_required'`, `psp`): the paused
850
+ request names no amount and no page total reached the SDK. For Pelecard the
851
+ total its payment page shows is what ties `amount` to what the page charges,
852
+ so it is required beside `amount`. Thrown before any authorization exists,
853
+ and not terminal: the page's next submission reads the total again.
463
854
 
464
855
  ## Building this package
465
856
 
@@ -641,6 +1032,29 @@ Allowed, the request continues untouched, the SDK emits `stripe_payment_continue
641
1032
  and the reason becomes `stripe_payment_continued`. One confirm continues per
642
1033
  approval; every other follow-up, including an unrelated intent, a changed
643
1034
  amount and a retry, stays blocked.
1035
+
1036
+ A card save can follow a `/v1/payment_methods` approval the same way, once
1037
+ Agentcard turns card-save continuations on for your company: the page's
1038
+ SetupIntent confirm pays with the approved payment method. A Payment Element
1039
+ page that creates the method itself (`paymentMethodCreation: 'manual'`, then
1040
+ `confirmSetup({ elements, confirmParams: { payment_method } })`) confirms with
1041
+ that method and no card, and the SDK continues the confirm untouched once the
1042
+ API allows it. A page whose confirm carries the card again (`confirmCardSetup`
1043
+ with the Card Element after `createPaymentMethod`) has it sent with every
1044
+ `payment_method_data` field removed and the approved `payment_method` in their
1045
+ place: the SDK asks the API with exactly that body and continues the page's
1046
+ request with it. The API allows it only for an approval the cardholder gave on
1047
+ the Vault page, not by voice, with no `amount` (a SetupIntent charges
1048
+ nothing), paid with exactly the approved payment method on the same Stripe
1049
+ account, and a SetupIntent Stripe reports still confirmable. Allowed, the SDK
1050
+ emits `stripe_setup_continued` with
1051
+ `{ authorizationId, setupIntentId, rewritten }`, where `rewritten` is true
1052
+ when the SDK wrote the approved payment method in place of a card the confirm
1053
+ carried, and the reason becomes `stripe_setup_continued`; the merchant then
1054
+ keeps the card for later charges. Until your company is turned on, the API
1055
+ refuses the check (`continuation_invalid`) and the confirm is aborted, as
1056
+ before. Either kind of intent uses up the approval's one continuation.
1057
+
644
1058
  Unrecognized merchant-server endpoints remain outside this guard unless listed
645
1059
  in `paymentEndpoints`; this is not a guarantee against a merchant charging a
646
1060
  saved token on its own server.
@@ -694,6 +1108,8 @@ await page.getByRole('button', { name: 'Pay', exact: true }).click();
694
1108
  | 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
1109
  | 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
1110
  | 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 |
1111
+ | 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)" |
1112
+ | 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
1113
 
698
1114
  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
1115
 
@@ -734,6 +1150,18 @@ After approval, submit the form once. The Vault replaces the card fields on your
734
1150
 
735
1151
  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
1152
 
1153
+ Prepare an Amadeus Checkout card step before the agent clicks Pay:
1154
+
1155
+ ```ts
1156
+ await controller.prepare({ psp: 'amadeus_checkout', environment: 'production' }); // 'sandbox' for Amadeus's test platforms
1157
+ ```
1158
+
1159
+ 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.
1160
+
1161
+ 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.
1162
+
1163
+ 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.
1164
+
737
1165
  Prepare Recurly before clicking the merchant's payment button:
738
1166
 
739
1167
  ```ts
@@ -742,6 +1170,49 @@ await controller.prepare({ psp: 'recurly', environment: 'shared' });
742
1170
 
743
1171
  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
1172
 
1173
+ ### Pay an ACI COPYandPAY checkout
1174
+
1175
+ 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.
1176
+
1177
+ Prepare before Pay. ACI's widget gives up 60 seconds after Pay:
1178
+
1179
+ ```ts
1180
+ await vault.syncRegistry();
1181
+ const controller = await attachToPlaywright(page, { vault, user, merchant, amount: 4210, currency: 'GBP' });
1182
+ await controller.prepare({ psp: 'aci', environment: 'production' }); // 'sandbox' for eu-test.oppwa.com and test.oppwa.com
1183
+ ```
1184
+
1185
+ 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:
1186
+
1187
+ - 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.
1188
+ - 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.
1189
+ - lets the holder request through untouched.
1190
+ - 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.
1191
+
1192
+ 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.
1193
+
1194
+ The `blocked` events the SDK adds for ACI, by `detail`:
1195
+
1196
+ | `detail` | When |
1197
+ |---|---|
1198
+ | `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. |
1199
+ | `aci_unsupported_request` | A leg the widget never sends, or a `/payment` that a page script sends itself instead of the widget's form. |
1200
+ | `aci_checkout_reused` | A second card-number request arrives on a checkout whose first one is running or was answered in this attachment. |
1201
+ | `aci_leg_unanswered` | A CVC or expiry request's card-number request failed, or none began within 5 seconds. |
1202
+ | `aci_payment_unapproved` | A `/payment` arrives before its checkout's card-number request was answered. |
1203
+ | `aci_payment_repeated` | A second `/payment` arrives on the same checkout. |
1204
+ | `aci_brand_mismatch` | `paymentBrand` is not the brand of the placeholder you typed. |
1205
+ | `aci_result_url_refused` | `shopperResultUrl` or `shopOrigin` is off the prepared merchant origin. |
1206
+ | `aci_expiry_unstrippable` | The SDK cannot remove the placeholder expiry: the fields are malformed, or the browser is not Chromium. |
1207
+ | `aci_payment_unsent` | The browser failed to send a `/payment` the SDK continued. The SDK never sends a second one. |
1208
+ | `checkout_interception_not_ready` | A `/payment` arrives after the checkout was cancelled or the attachment stopped. |
1209
+
1210
+ An unprepared card-number request throws `PreparationRequiredError` before any create. Click to Pay, wallet and summary-page forms of the widget are not supported.
1211
+
1212
+ 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.
1213
+
1214
+ 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.
1215
+
745
1216
  ### Use Checkout Pro in Mexico
746
1217
 
747
1218
  Attach the SDK before entering card fields on `www.mercadopago.com.mx`, then prepare the guest card checkout:
@@ -760,7 +1231,7 @@ Braintree's native `ClientConfiguration` GraphQL query can run before, during or
760
1231
 
761
1232
  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
1233
 
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.
1234
+ 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
1235
 
765
1236
  Lost authorization polling, local approval timeouts, or interrupted browser
766
1237
  handoffs produce `outcome_unknown` and block automatic retry. The thrown
@@ -854,8 +1325,8 @@ event, `unreadable_gmo_request`, and the next one is judged again.
854
1325
 
855
1326
  ### Pay a CyberSource Flex Microform checkout
856
1327
 
857
- Amtrak, Sonos and Learning Without Tears take cards in CyberSource Flex
858
- Microform. Each card field is CyberSource's own iframe, and on createToken the
1328
+ Amtrak, Sonos, Learning Without Tears and Alaska Airlines take cards in
1329
+ CyberSource Flex Microform. Each card field is CyberSource's own iframe, and on createToken the
859
1330
  field seals what the agent typed into a JWE under a capture context the merchant
860
1331
  minted for this checkout. Nothing in that body can be swapped, so the
861
1332
  cardholder's device seals the real card under the same context and sends it to
@@ -926,12 +1397,695 @@ const checkout = await attachToPlaywright(page, {
926
1397
  agent types the last card field; `sandbox` is `testflex.cybersource.com` with
927
1398
  CyberSource's test keys, and a test capture context pays only from a test
928
1399
  client.
1400
+ - **Seconds, not thirty.** Some pages race createToken against a timer of
1401
+ their own: Alaska Airlines' flight checkout gives it three seconds, then
1402
+ shows "Token creation timed out". After Pay, the cardholder's device would
1403
+ still have to seal the card and reach CyberSource, and the SDK's first read
1404
+ comes two seconds after the create. For such a page, prepare once the card
1405
+ fields show, with `tokenizeBeforePay`:
1406
+
1407
+ ```ts
1408
+ // The agent has typed the placeholder card and the expiry; the fields are loaded.
1409
+ const prepared = await checkout.prepare({ psp: 'cybersource_flex', environment: 'production', tokenizeBeforePay: true });
1410
+ await page.getByRole('button', { name: 'Pay', exact: true }).click();
1411
+ ```
1412
+
1413
+ The attachment reads the capture context and the fields of the one card
1414
+ Microform on the page and sends them with the preparation. When the
1415
+ cardholder approves, their device seals the card under that context, gets
1416
+ CyberSource's transient token and keeps it; at Pay the SDK binds the field's
1417
+ request, reads every 100 ms for 10 seconds, and answers the page as soon as
1418
+ the device hands the token over. Only a request from the same fields, under
1419
+ the same capture context, binds: a page that loads its fields again (Alaska
1420
+ refreshes its context every 15 minutes) fails with `checkout_changed`, so
1421
+ prepare again. `prepare()` is refused before any preparation exists with
1422
+ `microform_not_loaded` (no card field yet), `microform_ambiguous` (two card
1423
+ Microforms), `microform_unsupported`, `capture_context_invalid` or
1424
+ `cybersource_environment_mismatch`. The token the device holds authorizes
1425
+ nothing on its own: if the agent never clicks Pay, it expires unused.
929
1426
  - **One createToken per purchase.** A prepared approval binds one request. On
930
1427
  Amtrak, leave "add to profile" unticked or check out as a guest: that box
931
1428
  calls createToken a second time before Pay.
932
1429
  - Autopilot never pays a Flex checkout; the cardholder approves each one on
933
1430
  their device.
934
1431
 
1432
+ ### Pay a Juspay checkout
1433
+
1434
+ Swiggy's website pays cards through Juspay Express Checkout, from the page's
1435
+ own code. The page first sends the card to Juspay's `/card/tokenize`, then pays
1436
+ the order at `/txns` with the token it got back and the security code. A Juspay
1437
+ card token pays any order at that merchant for 15 minutes with no merchant
1438
+ secret, so the agent's browser never holds a real one, and the placeholder the
1439
+ agent typed never reaches Juspay.
1440
+
1441
+ ```ts
1442
+ await vault.syncRegistry(); // the Juspay recognizer is never in the built-in list
1443
+ const checkout = await attachToPlaywright(page, {
1444
+ vault, user: 'your-user-id', merchant: 'Swiggy', amount: 49900, currency: 'INR',
1445
+ });
1446
+ // The agent types a placeholder card and clicks Pay on the merchant's page.
1447
+ ```
1448
+
1449
+ 1. The SDK answers the page's `/card/tokenize` itself with a stand-in token
1450
+ (`ctkn_agentcard` and 32 hex digits) that pays nothing anywhere, and refuses
1451
+ the page's `/v2/card/fingerprint` lookup of the placeholder. Swiggy's page
1452
+ carries on without the fingerprint.
1453
+ 2. The `/txns` that carries that stand-in token pauses, and Agentcard asks the
1454
+ cardholder to approve.
1455
+ 3. On approval, the cardholder's device tokenizes the real card at Juspay and
1456
+ sends the payment itself. The real token never leaves that device.
1457
+ 4. The paused `/txns` receives Juspay's answer, cut down to the order id, the
1458
+ transaction id, the status and the bank's verification redirect. The page
1459
+ then opens the bank's check (3DS or a one-time code) in your browser. Report
1460
+ it with `checkout.requestUserAction('3ds')` and confirm the order with the
1461
+ merchant: an approval is not a payment.
1462
+
1463
+ - `syncRegistry()` declares `juspay_txns.v1`, and the API serves the Juspay
1464
+ recognizer only for an org Agentcard has enabled, so only then does the SDK
1465
+ take over a Juspay request. For any other org the SDK does not take over
1466
+ Juspay's requests, the same as for a processor it does not pay, and a create
1467
+ for a Juspay request answers `403 processor_unavailable`.
1468
+ - Net banking, UPI and wallet payments, a card the shopper saved with the
1469
+ merchant sent with only its stored token and security code, the bank's
1470
+ redirect, CORS preflights and reads pass untouched.
1471
+ - The SDK refuses these before any authorization exists: saving the card
1472
+ (`save_to_locker=true`), `tokenize=true`, EMI, a `/txns` that carries the
1473
+ card number, a saved card's payment that carries a new card's fields, and
1474
+ any card request to `api.juspay.io` or `api-ns1.juspay.in`. The controller
1475
+ reads `unsupported` with reason `juspay_request_unsupported`, and the
1476
+ `unsupported_checkout` event names `processor: 'juspay'` and the `reason`.
1477
+ With `onEvent: console.log` on the attachment above, a payment with "save
1478
+ card" ticked logs these three events, and `console.log(checkout.getState())`
1479
+ then prints the last object:
1480
+
1481
+ ```text
1482
+ { type: 'blocked', detail: 'juspay_placeholder_fingerprint' }
1483
+ { type: 'checkout_prepared', detail: { processor: 'juspay' } }
1484
+ {
1485
+ type: 'unsupported_checkout',
1486
+ detail: {
1487
+ url: 'https://api.juspay.in/txns',
1488
+ method: 'POST',
1489
+ processor: 'juspay',
1490
+ reason: 'save_to_locker'
1491
+ }
1492
+ }
1493
+ {
1494
+ status: 'unsupported',
1495
+ authorizationId: null,
1496
+ reason: 'juspay_request_unsupported'
1497
+ }
1498
+ ```
1499
+
1500
+ Untick "save card" and attach again.
1501
+ - The SDK refuses three requests alone, each with a `blocked` event: the
1502
+ fingerprint lookup (`juspay_placeholder_fingerprint`), a `/txns` whose
1503
+ stand-in token this attachment never issued (`juspay_unknown_synthetic_token`),
1504
+ and a card request whose body the browser did not hand over
1505
+ (`juspay_request_body_unreadable`).
1506
+ - Each `/txns` names its own order. If the page gives up on a paused `/txns`
1507
+ and the agent clicks Pay again, the SDK never answers the new order from the
1508
+ earlier approval: the attachment holds as `outcome_unknown` with reason
1509
+ `merchant_request_aborted` until you reconcile the merchant order.
1510
+ - The page decides card eligibility and card offers from the number the agent
1511
+ typed. Type a documented test card of the same network as the stored card,
1512
+ and take no card or bank offers.
1513
+ - Juspay's sandbox host (`sandbox.juspay.in`) takes only a test authorization,
1514
+ paid with a documented test card. The API refuses a live authorization naming
1515
+ it with `400 sandbox_host_on_live_row`.
1516
+ - Juspay's hosted payment page (HyperCheckout, used by IndiGo) sends the card
1517
+ from Juspay's own page, which the SDK does not take over. Autopilot never
1518
+ pays a Juspay checkout; the cardholder approves each one on their device.
1519
+
1520
+ ### Pay a Datatrans Secure Fields checkout
1521
+
1522
+ British Airways takes cards in Datatrans Secure Fields (PCI Proxy). The card
1523
+ number and CVV are Datatrans's own iframes on `pay.datatrans.com`
1524
+ (`pay.sandbox.datatrans.com` in test), and the card-number iframe sends the
1525
+ whole card in one request: `POST /upp/payment/SecureFields/paymentField`,
1526
+ form encoded, in TOKENIZE mode. The SDK pauses that request, the cardholder's
1527
+ device sends the real card to Datatrans, and the iframe receives Datatrans's
1528
+ token (`transactionId`) and its `cardInfo`: brand, credit or debit, consumer
1529
+ or corporate, issuing country and issuer, never a digit of the card.
1530
+
1531
+ ```ts
1532
+ await vault.syncRegistry(); // the Datatrans recognizer is never in the built-in list
1533
+ const checkout = await attachToPlaywright(page, {
1534
+ vault, user: 'your-user-id', merchant: 'British Airways', amount: '189.90', currency: 'GBP',
1535
+ });
1536
+ ```
1537
+
1538
+ - **Dark launch.** `syncRegistry()` declares the
1539
+ `datatrans_secure_fields_tokenize` capability, and the API serves the
1540
+ Datatrans recognizer only to an org Agentcard has enabled for it. Every
1541
+ other org's SDK leaves Datatrans requests alone, as before.
1542
+ - **Pass the amount.** The card request names no amount: the merchant charges
1543
+ the token later, from its own server. The `amount` and `currency` you pass,
1544
+ or the page total when you pass none, are what the approval shows. Without
1545
+ either the create answers `amount_required`.
1546
+ - **What pauses.** Only the card-number iframe's TOKENIZE post of a new card.
1547
+ A post Datatrans's own iframe would not send for that flow (another mode, a
1548
+ payment or 3-D Secure post, a key the iframe never writes, a URL variant, a
1549
+ content type other than jQuery's form type) is aborted with a `blocked`
1550
+ event, detail `invalid_datatrans_request`. Nothing is created, nobody is
1551
+ prompted, the controller's state does not change, and the page's next post
1552
+ is judged afresh. Wallet, bank-account and CVV-only posts carry no card
1553
+ number and continue untouched, and so do the iframe documents and the BIN
1554
+ lookup (`paymentField/cardInfo`).
1555
+ - **Card on file.** A post with `usage=REGISTRATION` asks PCI Proxy for a
1556
+ reusable alias and a network token, so the merchant could charge the card
1557
+ again without another approval. The API serves Agentcard's card-on-file
1558
+ policy with the Datatrans recognizer (`cardOnFile`), and the SDK reads it at
1559
+ each `syncRegistry()`. While it is `refuse`, as it is today, the post is
1560
+ aborted with a `blocked` event, detail `datatrans_card_on_file_refused`,
1561
+ before any create. British Airways's Nexus checkout sends it for every new
1562
+ card, so a Nexus checkout is refused this way for now. When the policy
1563
+ allows card on file, the post pauses like any other and the approval
1564
+ applies the policy; no SDK release is needed.
1565
+ - **The same form, submitted again.** British Airways submits the form again
1566
+ whenever a field loses focus or the expiry changes. Once a post was
1567
+ approved, a later post from the same iframe that differs from it only in
1568
+ the expiry (`expm`, `expy`) and the browser details is answered with the
1569
+ same token and `cardInfo`: no second prompt, no second card sent. The
1570
+ adapters emit `repeat_answered` with the `authorizationId`; it is not a
1571
+ second payment and must not be counted as one. Any other change (a new
1572
+ `formId` from a reloaded iframe, another placeholder card or CVV, another
1573
+ email or phone number) is a new approval. The answer lasts while that
1574
+ approval is the checkout's current one, up to the token's 30-minute
1575
+ lifetime: once the order completes or fails, the attachment stops, or
1576
+ another authorization starts, the form is judged again like any card post.
1577
+ It also ends once the form has posted nothing for five minutes. Once the
1578
+ token reached the page, the merchant can already hold an order paid with
1579
+ it, and the post names no amount or order, so before each later re-submit
1580
+ the SDK calls your `resolveMerchantResult` and answers only while it
1581
+ reports `pending`; that call changes no checkout state. A completed,
1582
+ failed or unknown order, a resolver that throws, or one that has not
1583
+ answered within five seconds makes the re-submit a new question, and an
1584
+ attachment with no `resolveMerchantResult` answers no
1585
+ re-submit after the token reached the page: each one asks the cardholder
1586
+ again.
1587
+ - **Type a placeholder of the card's brand.** The iframe names the brand
1588
+ (`paymentMethod`) from the digits the agent types, and British Airways's page
1589
+ sets its CVV length and contact fields by that brand, so the cardholder's
1590
+ device refuses a card whose brand differs, before anything is sent.
1591
+ Agentcard-issued cards are Mastercard: type a Mastercard test number.
1592
+ - **Merchant accounts.** A token minted into a Datatrans account can be read
1593
+ back as the card number by that account's holder, so the API refuses a
1594
+ merchant account Agentcard has not reviewed (`datatrans_merchant_unregistered`).
1595
+ - Autopilot never pays a Datatrans checkout; the cardholder approves each one
1596
+ on their device. `pay.sandbox.datatrans.com` takes only a test-mode client
1597
+ and `pay.datatrans.com` only a live one.
1598
+
1599
+ ### Pay a Pelecard checkout
1600
+
1601
+ Pelecard's hosted payment page (Iframe/Redirect 2.0) runs at
1602
+ `https://gateway21.pelecard.biz/PaymentGW?transactionId=...` (`gateway20` on
1603
+ older terminals), in the merchant's iframe or top level. When the buyer clicks
1604
+ Pay, the page sends the card in one XHR to its own `/PaymentGW/Transaction`.
1605
+ The SDK pauses that request, the cardholder approves on their phone, their
1606
+ device sends the real card to Pelecard, and the page receives Pelecard's
1607
+ answer, so its own script takes the buyer to the merchant's landing page.
1608
+
1609
+ Pelecard ships dark: `syncRegistry()` asks for the `pelecard_transaction`
1610
+ capability, and the API serves the recognizer only to an org Agentcard has
1611
+ enabled for Pelecard, in test mode only. For every other org the SDK leaves
1612
+ Pelecard's requests alone.
1613
+
1614
+ ```ts
1615
+ await vault.syncRegistry();
1616
+ const checkout = await attachToPlaywright(page, {
1617
+ vault, user: 'user_123', merchant: 'Uman tourist tax',
1618
+ amount: 2700, currency: 'usd', // required: the request names no amount
1619
+ pageAmount: async () => {
1620
+ // Required: the total Pelecard's page shows, which the API holds against `amount`.
1621
+ const frame = page.frames().find((f) => /^https:\/\/gateway2[01]\.pelecard\.biz\/PaymentGW\b/.test(f.url()));
1622
+ const shown = await frame?.evaluate(() => {
1623
+ const p = (globalThis as any).generalParameters;
1624
+ return p ? { total: p.total, currencyCode: p.currencyCode } : null;
1625
+ });
1626
+ if (typeof shown?.total !== 'number' || typeof shown?.currencyCode !== 'string') return undefined;
1627
+ return { amount: Math.round(shown.total * 100), currency: shown.currencyCode.toLowerCase() }; // two-decimal currencies
1628
+ },
1629
+ });
1630
+ ```
1631
+
1632
+ - Pass `amount` and `currency`. Pelecard bound the total, the currency and
1633
+ the action (a charge, a card registration or a hold) to the payment page
1634
+ when the merchant created it, so the submission names none of them. The
1635
+ pair you pass is what the cardholder approves; without it the submission
1636
+ fails with `AmountRequiredError` before anything is created. No Pelecard
1637
+ answer states an amount either, so confirm the order with the merchant.
1638
+ - Pass the page's total as `pageAmount`. It is the one figure that ties
1639
+ `amount` to what the merchant bound to the payment page, so a submission
1640
+ without it fails with `PageAmountRequiredError` before anything is created,
1641
+ and the page's next submission reads it again. The API refuses a create
1642
+ without it (`page_amount_required`), a total of 0 (a card registration or a
1643
+ hold of an amount the page does not show) and a total that disagrees with
1644
+ `amount` (`amount_mismatch`).
1645
+ - Before any prompt, the SDK refuses a submission that asks Pelecard to keep
1646
+ the card on file (the buyer ticked "save my card", or the body sets
1647
+ `CreateToken`) as `pelecard_keep_on_file_unsupported`: the merchant could
1648
+ charge that card again without another approval, and the approval cannot
1649
+ ask the cardholder yet. Anything else the page's one-time card payment
1650
+ never sends (a saved token, a card track, a phone authorization,
1651
+ installments, a buyer-typed total, a key the page does not build) is
1652
+ `invalid_pelecard_request`. Both arrive as a `blocked` event, the page's
1653
+ request is aborted, nothing is created, and the page's next submission is
1654
+ judged afresh. A merchant can still set `CreateToken` when it creates the
1655
+ payment page, which no request shows.
1656
+ - The page's other tabs post to the same URL with no card (Bit, wallets, the
1657
+ PIN pad, bank transfer) and continue untouched.
1658
+ - The page waits up to ten minutes for the answer, and Pelecard's payment
1659
+ session is expected to last about as long from page load, so the approval
1660
+ has to land inside it. A 3-D Secure answer is never read as approved:
1661
+ reconcile that payment with the merchant.
1662
+
1663
+ ### Pay a CreditGuard checkout
1664
+
1665
+ 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:
1666
+
1667
+ ```ts
1668
+ const pps = page.frames().find(frame => /^https:\/\/(pps|pps2|ppsuat)\.creditguard\.co\.il\/\?/.test(frame.url()));
1669
+ await controller.prepare({ psp: 'creditguard', environment: 'production', paymentPageUrl: pps!.url() });
1670
+ // 'sandbox' for CreditGuard's UAT page on ppsuat.creditguard.co.il
1671
+ ```
1672
+
1673
+ 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.
1674
+
1675
+ 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.
1676
+
1677
+ 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.
1678
+
1679
+ ### Pay a Primer checkout
1680
+
1681
+ Primer launches dark: `syncRegistry()` declares the
1682
+ `primer_payment_instruments` capability (in `SUPPORTED_CAPABILITIES`), and the API
1683
+ serves the Primer recognizer only to an org Agentcard has enabled for Primer.
1684
+ For every other org nothing changes: Primer's requests are never paused and
1685
+ never refused.
1686
+
1687
+ For an enabled org, the SDK pauses one request: the card tokenization that
1688
+ Primer Web SDK v2 (Printful loads v2.63.0) sends from its hidden api-controller
1689
+ frame on `https://sdk.primer.io` when the shopper pays with a new card. That is a
1690
+ `POST` to `https://sdk.api.primer.io/payment-instruments` (sandbox
1691
+ `sdk.api.sandbox.primer.io`) as JSON, with the card at
1692
+ `paymentInstrument.number`. The frame is cross-origin, and both adapters pause
1693
+ the request inside it. The cardholder's device fills the real card into the
1694
+ paused body and sends it to Primer itself. The page receives only Primer's
1695
+ answer: one single-use card token, rebuilt from an allow-list and handed over
1696
+ under a JSON content type. Any other answer on an approved authorization, such
1697
+ as a multi-use token or a saved card, throws `PaymentOutcomeUnknownError` with
1698
+ reason `primer_token_unconfirmed`.
1699
+
1700
+ ```ts
1701
+ await vault.syncRegistry();
1702
+ const controller = await attachToPlaywright(page, {
1703
+ vault, user: 'usr_123', merchant: 'Printful',
1704
+ amount: 2599, currency: 'usd', // Primer's request names no amount
1705
+ });
1706
+ ```
1707
+
1708
+ Primer's request names no amount: Primer's SDK or the merchant's server charges
1709
+ the token later. Pass `amount` and `currency`, or let the page total
1710
+ (`pageAmount`, or `[data-agentcard-amount]`) name the price: that is what the
1711
+ cardholder approves. With neither, the API answers 400 `amount_required` before
1712
+ anything is created, and the attachment reads `failed`. A 3-D Secure challenge
1713
+ that follows the token runs in your browser: call `requestUserAction('3ds')`
1714
+ when you see one.
1715
+
1716
+ Primer's SDK stamps `preferredNetwork` from the digits typed, and the
1717
+ cardholder's device refuses one that is not the paying card's own network. Type
1718
+ a placeholder of the same network as the card that pays.
1719
+
1720
+ The other requests on Primer's hosts:
1721
+
1722
+ - A wallet tokenization with no card (PayPal, Apple Pay, Google Pay, Klarna) and
1723
+ a saved-card exchange continue untouched.
1724
+ - A tokenization that asks Primer to save the card (`tokenType: "MULTI_USE"` or
1725
+ `paymentFlow: "VAULT"`) is aborted alone, with a `blocked` event,
1726
+ `primer_keep_on_file`. The attachment is not held: clear the page's save-card
1727
+ choice and pay again.
1728
+ - A card request from Primer Checkout (`@primer-io/primer-js`), whose frame gives
1729
+ up on its own request after 30 seconds, is aborted as `unsupported_checkout`
1730
+ with reason `unsupported_primer_sdk`, and any other card request the v2 frame
1731
+ would not send with `unsupported_primer_request`. The attachment reads
1732
+ `unsupported` with that reason and holds, since the merchant's integration
1733
+ decides that shape.
1734
+ - A request whose body the SDK cannot read is aborted alone, with a `blocked`
1735
+ event, `unreadable_primer_request`.
1736
+
1737
+ A client session the merchant created to save every card after payment (Printful
1738
+ does this for a signed-in shopper) shows nothing in the request, so the SDK
1739
+ cannot detect that save. Until the cardholder can consent to it, Primer pays
1740
+ only for a test-mode client: for a live client the API answers 400
1741
+ `invalid_primer_request` with reason `live_keep_on_file` before anything is
1742
+ created, the SDK aborts the paused request with nothing sent, and the attachment
1743
+ reads `failed`.
1744
+
1745
+ ### Pay a Payrails checkout
1746
+
1747
+ Payrails (web SDK v6 secure fields, as on Blacklane's consumer booking checkout)
1748
+ launches dark: `syncRegistry()` asks for the `device_encrypt` mode and declares
1749
+ the `payrails_device_encrypt` capability, and the API serves the Payrails
1750
+ recognizer only to an org Agentcard has enabled for Payrails. For every other
1751
+ org nothing changes: no answer is read, no Payrails host is intercepted, and
1752
+ Payrails' requests go through untouched.
1753
+
1754
+ For an enabled org, the SDK pauses one request: the card form's save, a `POST`
1755
+ to the session's `saveInstrument` link (`https://api-pub.payrails.io/public/payment/instruments`;
1756
+ test hosts `api-pub.staging.payrails.io` and `sandbox-api-pub.staging.payrails.io`)
1757
+ that carries the whole card as one JWE. The key it was encrypted under is not in
1758
+ that request, so the adapters read the Payrails init payload (`{version, data}`)
1759
+ from the page's own XHR and fetch answers and keep, in memory only, up to eight
1760
+ sessions for fifteen minutes. A paused save binds to the one session whose token,
1761
+ save link, `holderReference` and `providerConfigId` it names, seen at most nine
1762
+ minutes before, and that session's key rides on the create. The cardholder's
1763
+ device encrypts the real card under it and sends the save to Payrails itself;
1764
+ the page receives Payrails' 201 rebuilt from an allow-list (the instrument, its
1765
+ status, the network, the BIN cut to six digits, the last four, the expiry, the
1766
+ holder name and the fingerprint). Call `syncRegistry()` before attaching, or the
1767
+ page's session answers are not read.
1768
+
1769
+ ```ts
1770
+ await vault.syncRegistry();
1771
+ const controller = await attachToPlaywright(page, {
1772
+ vault, user: 'usr_123', merchant: 'Blacklane',
1773
+ amount: 8900, currency: 'eur', // the save names no amount: this, or the page total, is what the cardholder approves
1774
+ });
1775
+ ```
1776
+
1777
+ A save names no amount (the merchant charges the saved card later), so pass
1778
+ `amount` and `currency`, or a `pageAmount` reader: without either, the save
1779
+ throws before anything is created.
1780
+
1781
+ What the SDK refuses, before anything is created, with the reason on the
1782
+ `unsupported_checkout` event:
1783
+
1784
+ - A save that keeps the card on file (`storeInstrument: true` or
1785
+ `UnscheduledCardOnFile`, which Blacklane sends on every save):
1786
+ `payrails_card_on_file_consent_required`, until the cardholder can consent to
1787
+ it. A `Subscription` save (`payrails_future_usage_refused`) and a body that is
1788
+ not the Payrails SDK's own (`payrails_request_malformed`,
1789
+ `payrails_not_card_save`, `payrails_jwe_malformed`) are refused too. The
1790
+ attachment reads `unsupported` with that reason and holds, since the merchant's
1791
+ integration decides that shape.
1792
+ - Paying with the card form or a stored card with a CVC field, which sends the
1793
+ card inside `paymentComposition` to any Payrails host:
1794
+ `unsupported_payrails_card_payment`. The attachment holds, and the agent's
1795
+ placeholder card never reaches an authorization.
1796
+ - A save no captured session matches (`payrails_session_unavailable`), or whose
1797
+ captured key is not the one the page's secure frame used (`payrails_key_invalid`,
1798
+ `payrails_key_mismatch`): that request alone is aborted, the event carries
1799
+ `message: 'Payrails session expired, reload the checkout'`, and the save after
1800
+ a reload is judged afresh.
1801
+
1802
+ A save with no card (a dynamic payment-method form posts to the same link)
1803
+ continues untouched. A `POST` to a Payrails host with no body the SDK can read,
1804
+ whether it has none, an empty one or a streamed one, is aborted alone with a
1805
+ `blocked` event, `unreadable_payrails_request`, by both adapters: Playwright
1806
+ reports all three the same way. The Payrails SDK sends a JSON body with every
1807
+ `POST`, so none of its own requests is refused this way. Payrails' static host,
1808
+ `assets.payrails.io`, serves only files and takes no payment, so a bodyless
1809
+ `POST` another script sends there continues untouched. The matching API,
1810
+ Vault and database releases are required, and the API also refuses a save for a
1811
+ merchant whose Payrails workspace Agentcard has not reviewed.
1812
+
1813
+ ### Pay an Elavon Converge payment page
1814
+
1815
+ Elavon Converge's Hosted Payment Page is where NY DMV's payment apps send the
1816
+ browser: Converge's own page at
1817
+ `https://api.convergepay.com/hosted-payments/?ssl_txn_auth_token=...`, at top
1818
+ level. When the buyer clicks Pay, the page posts the card as one JSON request
1819
+ to its own `/hosted-payments/service/payment/hpp/process`. The SDK pauses that
1820
+ request, the cardholder approves the amount it names on their phone, their
1821
+ device sends the real card to Converge, and the page receives Converge's
1822
+ approval, so its own script shows the receipt and returns the buyer to the
1823
+ merchant. Converge's demo environment serves the same page on
1824
+ `api.demo.convergepay.com`, which pays in test mode only.
1825
+
1826
+ Converge ships dark: `syncRegistry()` asks for the `converge_hpp_process`
1827
+ capability, and the API serves the recognizer only to an org Agentcard has
1828
+ enabled for Converge. For every other org the SDK leaves Converge's requests
1829
+ alone.
1830
+
1831
+ ```ts
1832
+ await vault.syncRegistry();
1833
+ const checkout = await attachToPlaywright(page, {
1834
+ vault, user: 'user_123', merchant: 'NY DMV',
1835
+ // No amount: the request names its own (ssl_amount, US dollars), and that is what the cardholder approves.
1836
+ });
1837
+ ```
1838
+
1839
+ - No `amount` is needed. The request's `ssl_amount` is the amount the
1840
+ cardholder approves; an `amount` and `currency` you pass must agree with it.
1841
+ - Every Converge payment waits for the cardholder's own approval. An autopilot
1842
+ grant never pays one.
1843
+ - Electronic check and financing payments on the same URL carry no card and
1844
+ continue untouched.
1845
+ - Before any prompt, the SDK refuses every other shape on that URL with
1846
+ payment-core's own code (`ConvergeRefusalReason`, listed in
1847
+ `CONVERGE_REFUSAL_REASONS`), the one the API and the cardholder's device
1848
+ would answer: a wallet, the debit, gift card and push-to-card tabs, a
1849
+ follow-up after a 3-D Secure, Pay Defense or surcharge step, a CAPTCHA, a
1850
+ public-sector service fee, recurring or installment fields, card saving, a
1851
+ tip or cash back, another currency, no amount, no page session, and anything
1852
+ that is not the page's own request. The page's request is aborted, nothing is
1853
+ created, and the attachment is not held, so the page's next payment is judged
1854
+ afresh. The table below gives each code's meaning and the next step.
1855
+ - A terminal with 3-D Secure 2, Pay Defense or a surcharge answers the first
1856
+ request with a step instead of a result. The Vault never hands that answer to
1857
+ the page: the authorization ends declined as a processor refusal with
1858
+ `converge_step_unsupported` and nothing charged, `authorize()` throws
1859
+ `ProcessorRefusedError` with that `pspErrorCode`, and the page shows its own
1860
+ decline.
1861
+ - One payment per payment page. A second authorization for the same page
1862
+ session or invoice gets `duplicate_submission` for 15 minutes after the
1863
+ first card could last have left the device: confirm the payment with the
1864
+ merchant before trying again.
1865
+
1866
+ A refused payment reaches your `onEvent` as a `blocked` event whose `detail` is
1867
+ the code. This is what `onEvent` received when the agent paid with Google Pay
1868
+ on Converge's page, printed by the SDK's own Chromium test (in
1869
+ `packages/checkout`: `npm run build`, then
1870
+ `node elavon-converge-browser.test.mjs "playwright Google Pay"`):
1871
+
1872
+ ```text
1873
+ PASS playwright Converge Google Pay, then the card form; onEvent received {"type":"blocked","detail":"converge_wallet_refused"}
1874
+ 1 local Chromium Converge scenarios passed; no Converge host, demo account or live merchant was contacted.
1875
+ ```
1876
+
1877
+ The page's own request then fails the way a dropped connection does (its XHR
1878
+ ends with status 0), and the page keeps its session, so the buyer can pay with
1879
+ the card form next.
1880
+
1881
+ #### Converge refusal codes
1882
+
1883
+ The first 22 codes arrive as the `blocked` event's `detail`, before any
1884
+ authorization exists, and the API answers a create that carries one with a 400
1885
+ of the same code. `converge_step_unsupported`, `converge_declined` and
1886
+ `converge_session_timeout` arrive after the cardholder approved, as
1887
+ `ProcessorRefusedError.pspErrorCode`. `duplicate_submission` is the create's
1888
+ 409, as `CheckoutApiError.code`.
1889
+
1890
+ | Code | What it means | What to do next |
1891
+ |---|---|---|
1892
+ | `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. |
1893
+ | `converge_debit_card_refused` | The payment came from the page's debit card tab. | Pay with the credit card form. |
1894
+ | `converge_gift_card_refused` | The payment came from the page's gift card tab. | Pay with the credit card form. |
1895
+ | `converge_push_to_card_refused` | The payment would send money to the card instead of charging it. | None: an Agentcard card never receives money. |
1896
+ | `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. |
1897
+ | `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. |
1898
+ | `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. |
1899
+ | `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. |
1900
+ | `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. |
1901
+ | `converge_amount_add_on_refused` | The payment adds a tip or cash back to `ssl_amount`. | Pay again without the tip or cash back. |
1902
+ | `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. |
1903
+ | `converge_recurring_refused` | The body sets up recurring or installment charges. | Pay the amount as one payment. |
1904
+ | `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. |
1905
+ | `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. |
1906
+ | `converge_context_missing` | The body carries no payment page session (`context`). | Start a new checkout from the merchant's page. |
1907
+ | `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. |
1908
+ | `converge_credential_refused` | The body names Converge account credentials, which the page never sends. | Let the page send its own request. |
1909
+ | `converge_card_missing` | The body carries no card number. | Type the placeholder card into the page's own card fields. |
1910
+ | `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. |
1911
+ | `converge_url_refused` | The request is not sent to the exact `hpp/process` URL. | Let the page send its own request. |
1912
+ | `converge_method_refused` | The request is not a POST. | Let the page send its own request. |
1913
+ | `converge_content_type_refused` | The request's content type is not JSON. | Let the page send its own request. |
1914
+ | `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. |
1915
+ | `converge_declined` | Converge declined the card. | Check the merchant's payment status, then start a new checkout to try again. |
1916
+ | `converge_session_timeout` | Converge's payment page session ended before the payment. Nothing was charged. | Start a new checkout from the merchant's page. |
1917
+ | `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. |
1918
+
1919
+ ### Prepare a Worldpay eProtect checkout (dark launch)
1920
+
1921
+ Worldpay eProtect (Vantiv, Litle PayPage) support ships dark. Worldpay's script
1922
+ encrypts the card in the page and sends it as the
1923
+ query of one JSONP `<script>` GET to `/eProtect/paypage` on
1924
+ `request.eprotect.vantivcnp.com`, or on `request.eprotect.vantivprelive.com`
1925
+ (Worldpay's pre-live host, the `sandbox` environment), then reads the answer
1926
+ from the script's own callback. `syncRegistry()` declares the
1927
+ `worldpay_eprotect_jsonp` capability, and the API serves the eProtect
1928
+ recognizer to a build that declares it only for an org Agentcard has enabled.
1929
+ For every other org the API serves no eProtect recognizer and answers
1930
+ `processor_unavailable` to an eProtect preparation or create, so the SDK leaves
1931
+ eProtect traffic exactly as it found it, as it does before a sync.
1932
+
1933
+ The merchant's script gives up on the registration 10 to 15 seconds after Pay,
1934
+ and re-sends it to a failover host after 5, which is too soon for a prompt that
1935
+ waits on a person. An eProtect checkout is therefore always prepared:
1936
+
1937
+ ```ts
1938
+ const checkout = await attachToPlaywright(page, {
1939
+ vault, user: 'your-user-id', merchant: 'Example merchant',
1940
+ amount: 4210, currency: 'usd',
1941
+ cardId: 'vc_...', // required: the stored card that pays
1942
+ onApprovalUrl: deliverPrivatelyToCardholder,
1943
+ });
1944
+ await checkout.prepare({ psp: 'worldpay_eprotect', environment: 'production', paypageId: 'MerchantPaypageId' });
1945
+ await page.getByRole('button', { name: 'Place order', exact: true }).click();
1946
+ ```
1947
+
1948
+ - `paypageId` is the merchant's eProtect paypageId: the Worldpay merchant the
1949
+ registration is for, which is also the merchant Worldpay lets charge the card
1950
+ again. It is fixed before the page sends anything, so a registration naming
1951
+ another paypageId never spends the approval. The Worldpay iframe names it in
1952
+ its URL (`/eProtect/payframe_<paypageId>_...`); a JS API merchant carries it
1953
+ in its eProtect configuration.
1954
+ - `cardId` is required, and the approval is locked to that card. Worldpay never
1955
+ receives an expiry: the merchant sends the one typed into its form with the
1956
+ registration. The agent types a placeholder number of the stored card's
1957
+ network, the security code `000`, and the stored card's real expiry
1958
+ (`expiry_month` and `expiry_year` from `GET /v2/vault_cards`) before it
1959
+ prepares.
1960
+
1961
+ After Pay, the SDK pauses the registration and binds it to the preparation.
1962
+ The cardholder's device, still on the approval page, encrypts the approved card
1963
+ under Worldpay's key into that same query and sends it through Agentcard's
1964
+ encrypted relay. The SDK then answers the paused script as Worldpay would:
1965
+ the registration in the script's own callback, under Worldpay's script type,
1966
+ with only the fields the merchant needs (no first six or last four digits).
1967
+ The SDK reads the authorization every 500 ms for 15 seconds, so the answer
1968
+ lands inside the merchant's window. A registration is not a payment: the
1969
+ merchant authorizes it with the typed expiry, so confirm the order with the
1970
+ merchant.
1971
+
1972
+ | Request on eProtect's hosts | What the adapters do |
1973
+ | --- | --- |
1974
+ | The registration GET naming the prepared `paypageId`, on the prepared environment's host | Paused, bound to the preparation and answered |
1975
+ | A registration with no ready preparation | Refused locally with `PreparationRequiredError`; nothing is created |
1976
+ | A registration for another `paypageId` or host | Refused, and the preparation is retired (`checkout_changed`) |
1977
+ | 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 |
1978
+ | 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` |
1979
+ | `/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`) |
1980
+ | The vendor's scripts, the payframe and every other report | Continue untouched |
1981
+
1982
+ Worldpay's script sends `900` or `901` when the merchant's timeout passed with
1983
+ no answer. The SDK then cancels the authorization, which the API confirms only
1984
+ before the device sent the registration, and fails the paused script instead of
1985
+ answering it. When the answer had already reached the page, the controller
1986
+ moves to `outcome_unknown`.
1987
+
1988
+ `prepare()` refuses locally with `CheckoutPreparationError` reason
1989
+ `paypage_id_invalid` (missing or not 1 to 64 letters and digits, or passed for
1990
+ another processor) or `card_required`, and with `ready_unconfirmed` when the
1991
+ API confirms another paypageId or card. A live checkout (`environment:
1992
+ 'production'`) needs a Worldpay merchant that payment-core pins for its
1993
+ paypageId at the merchant origin, and `merchant` must be that pin's name, the
1994
+ one the approval shows. The API refuses another name with
1995
+ `merchant_profile_mismatch` and a paypageId with no pin with
1996
+ `unsupported_checkout`, before any preparation exists; when this release pins
1997
+ the paypageId too, the SDK refuses another name first, before any network call.
1998
+ Agentcard has pinned no production merchant yet, so the API refuses every live
1999
+ eProtect checkout today; the pre-live host needs no pin. An approved answer that is not a
2000
+ registration for the paused request raises `PaymentOutcomeUnknownError` with
2001
+ reason `eprotect_registration_unconfirmed`: the device may have registered the
2002
+ card, so reconcile the merchant before trying again.
2003
+
2004
+ The SDK covers only Worldpay's own eProtect hosts, and does not pause a
2005
+ registration a merchant sends to an eProtect domain it names itself. The SDK
2006
+ does not support a merchant timeout of 5 seconds or less. The pre-live host
2007
+ receives only Worldpay's documented test cards. Autopilot never pays eProtect.
2008
+ The matching API, Vault and relay releases are required.
2009
+
2010
+ ### Pay a WSPay Components checkout
2011
+
2012
+ Hotel booking pages such as Valamar's take cards in WSPay Components, WSPay's
2013
+ build of Monri Components. The card field is WSPay's own iframe on
2014
+ `proxy.wspay.biz` (`proxy-test.wspay.biz` in test), and it sends the card in one
2015
+ request from that frame: `POST /v2/payment/{client_secret}/confirm`. The SDK
2016
+ pauses that request, the cardholder's device sends the real card to WSPay, and
2017
+ the frame receives WSPay's answer.
2018
+
2019
+ ```ts
2020
+ await vault.syncRegistry(); // the WSPay recognizer is never in the built-in list
2021
+ const checkout = await attachToPlaywright(page, {
2022
+ vault, user: 'your-user-id', merchant: 'Example Hotel', amount: '412.30', currency: 'EUR',
2023
+ });
2024
+ ```
2025
+
2026
+ - **Dark launch.** `syncRegistry()` declares the `wspay_confirm` capability,
2027
+ and the API serves the WSPay recognizer only to an org Agentcard has enabled
2028
+ for WSPay. Until then your SDK leaves every WSPay request as it did before
2029
+ this release: none is paused or refused. If Agentcard turns WSPay off for
2030
+ your org after a sync, the next create answers `403 processor_unavailable`
2031
+ and nothing is charged. `proxy-test.wspay.biz` takes only a test-mode client
2032
+ and `proxy.wspay.biz` only a live one.
2033
+ - **Pass the amount.** The confirm names no amount: WSPay fixed it when the
2034
+ merchant's server created the payment. The `amount` and `currency` you pass
2035
+ are what the approval shows. When WSPay approves at once, its answer names
2036
+ the amount and the API compares the two: a different amount reads
2037
+ `amountVerified: false` and the controller holds with reason
2038
+ `charged_amount_mismatch`. A 3-D Secure step names no amount, so nothing is
2039
+ compared on that path (below). Without `amount` and `currency` the confirm
2040
+ fails with `AmountRequiredError` before anything is created.
2041
+ - **What pauses.** Only the frame's confirm of a new card for a one-time
2042
+ payment, with the frame's own headers and body. A saved card (`saved_card`),
2043
+ a card tokenized earlier (`temp_card_id`), a card-shaped object the page
2044
+ built into the transaction, or any other request on that URL is aborted with
2045
+ a `blocked` event, detail `unsupported_wspay_payment_method`. Nothing is
2046
+ created, nobody is prompted, the controller's state does not change, and the
2047
+ page's next confirm is judged afresh. The frame's other requests (`/show`,
2048
+ `/status`, `/pan-info`) continue untouched.
2049
+ - **Keep on file.** A confirm with `tokenize_pan: true` asks WSPay to keep the
2050
+ card so the merchant can charge it later without another approval. Only the
2051
+ cardholder can agree to that and no approval asks them yet, so such a
2052
+ confirm is aborted with a `blocked` event, detail
2053
+ `wspay_keep_on_file_unsupported`, and nothing is created. Valamar sets it on
2054
+ every booking, so a Valamar checkout is refused this way until the approval
2055
+ asks the cardholder for that consent.
2056
+ - **3-D Secure.** When WSPay answers with its 3-D Secure step, the frame runs
2057
+ the bank's challenge in your browser, and nothing is charged until the
2058
+ cardholder completes it there. The controller reads `requires_user_action`
2059
+ with reason `3ds`, and `onUserAction` receives the same reason: show the
2060
+ challenge to the cardholder, then `reconcile()` the merchant's order. After
2061
+ the challenge WSPay charges the amount the merchant's server set, and no
2062
+ answer Agentcard receives names it: `amountVerified` stays `null`. Read the
2063
+ charged amount from the merchant's order and compare it with the one the
2064
+ cardholder approved before you report the purchase complete.
2065
+ - **The client_secret** in the confirm URL is a per-payment secret. `redactUrl`
2066
+ and every event report the URL as
2067
+ `https://proxy.wspay.biz/v2/payment/{client_secret}/confirm`.
2068
+ - Autopilot never pays a WSPay checkout; the cardholder approves each one on
2069
+ their device. Monri Components on `ipg.monri.com` and the WSPay Form hosted
2070
+ page are not covered.
2071
+
2072
+ ### Verifone merchants that encrypt the card in the page
2073
+
2074
+ 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:
2075
+
2076
+ - 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.
2077
+ - 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.
2078
+ - 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.
2079
+ - 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.
2080
+
2081
+ ```ts
2082
+ await controller.prepare({ psp: 'verifone', environment: 'production', merchantProfile: 'containerstore_checkout' });
2083
+ ```
2084
+
2085
+ 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.
2086
+
2087
+ 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.
2088
+
935
2089
  ### Unsupported endpoints
936
2090
 
937
2091
  `paymentEndpoints` is an explicit list supplied by the integrator after observing