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