npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agent-cards/sdk

v0.23.10

Published

Agentcard's SDK: one client, Agentcard, with two modules. checkout pauses a browser purchase and pays it with the person's own card; voice connects a person to your product once and approves a purchase by voice inside it. Card data never enters your proce

Readme

@agent-cards/sdk

Agentcard's SDK: one client, Agentcard, with two modules. The directory in the monorepo is still packages/checkout; the package on npm is @agent-cards/sdk. @agent-cards/checkout is its older name and re-exports this package, entry for entry. @agent-cards/voice is the phone package, not an alias of this one.

| Module | What it does | |---|---| | agentcard.checkout | Pauses a browser purchase's card request, has the cardholder approve it, and replays the processor's answer; creates a purchase directly; the recognisers and the request builders | | agentcard.voice | Connects a person's card to your product once; approves a purchase by voice inside it: the key word, the stream, the result; reads a returning person; revokes a connection |

Card data never enters your process, your logs or your network: your server holds a placeholder card and a token, and the person's device supplies the real card.

Install

npm i @agent-cards/sdk

Node 22 or any runtime with fetch, WebSocket and WebCrypto. Zero runtime dependencies. One base URL, https://api.agentcard.sh; sandbox and live are decided by your credentials.

Hello world

A person connects their card to your app once, then approves a purchase by voice. Six lines, against a sandbox client.

import { Agentcard } from '@agent-cards/sdk';

const agentcard = new Agentcard({ clientId: process.env.AGENTCARD_CLIENT_ID, clientSecret: process.env.AGENTCARD_CLIENT_SECRET, voice: { approval: 'sheet', returnUrl: 'yourapp://connected' } });

const link = await agentcard.voice.connect();                                   // the app opens link.url in its sheet
const person = await agentcard.voice.completeConnect(returnLink);               // the link the sheet came back with
const purchase = await agentcard.voice.authorizations.create({ user_id: person.user_id, merchant: 'Coffee', amount: 1250, currency: 'usd', request: 'sandbox' });
const { card, cards } = purchase;                                                // the card that pays and why; cards are the others, for authorizations.setCard(purchase.id, id)
const result = await agentcard.voice.awaitResult(purchase.id);                  // { status: 'approved', approved_with: 'voice' }

The app opens purchase.approval_url in its sheet for the approval (see @agent-cards/voice). Sandbox and live are decided by the credentials: the token exchange names the client's mode and the client reads it (agentcard.sandbox), so nothing here says which one this is. request: 'sandbox' is the first-run form: it names no processor, the sandbox completes it as a sandbox charge, and only a sandbox client may send it. At go-live the credentials change and the request becomes the processor's own card request: one a browser purchase paused on, or one agentcard.checkout.requests.build(...) made for your order. voice.returnUrl is exactly one of the redirect links registered on your client. readme.test.mjs runs this block against a stub on every test run.

A returning person

Store person.user_id. On a later launch ask what they already have, and open the Vault only for what is missing.

const who = await agentcard.voice.people.get(storedUserId);          // { user_id, card, voice: ['en'], rule, passkey }
if (!who.card || !who.voice.includes('en')) {
  const link = await agentcard.voice.connect({ voice: 'required' }); // the Vault shows only the missing step
}

connection_not_found means the person never connected through your client (or disconnected): treat them as new.

Approving inside your app

voice: { approval: 'sheet' }: the person approves on Agentcard's page, opened in your app for one moment; purchase.approval_url (or voice.approvals.approvalUrl(id)) is the link. voice: { approval: 'in_app' }: hold-to-talk in your app, judged on the phone and by Agentcard, signed by a key in your app. The server then mints the key word as one sealed bundle the app forwards unread:

const bundle = await agentcard.voice.challengeForHost(purchase.id, { lang: 'es' });   // to the app's VoiceHost.approve, unread
const reg = await agentcard.voice.appKeys.register(key, { user_id: person.user_id });  // the key the app's frame minted; reg.credential goes back to the phone

One choice per business, not per purchase; there is no default, and a client built without voice has every voice.* verb throw missing_approval_posture.

The card that pays

Under the person's smart routing (on unless they turned it off in the Vault) the card that earns the most for the purchase pays when your create pins none, and every read names it: purchase.card is { id, name, last4, brand, reason }, the reason the recommendation's sentence for it, null when your create pinned the card (card_id) or the person chose another. purchase.cards lists every card the person could pay with instead, each with recommended and the recommendation's reason where it scored the card. The key word line names the card and the reason before the word ("Paying 34 dollars and 99 cents to Epicdrops with your Sapphire, three times the points on shopping. Please say your secure passphrase, …"), and the bundle carries card so the app shows it beside the word: hearing the card, then speaking the passphrase, is the confirmation. "Another card", spoken or tapped, is voice.authorizations.setCard(purchase.id, card.id) before the approval; ask for the key word after it, so the line names the card that will pay. Once the purchase is decided the call answers already_finalized. A prepared checkout pays only with the card the cardholder approved for it: there the call answers card_not_stored for a card a company added and preparation_card_locked for any other card.

Where the key word goes

challengeForHost(id, { delivery }) names where the key word goes, and the bundle carries both of Agentcard's lines either way: say with the word in it, say_private pointing at the screen. delivery: 'screen': the app shows word, speaks say_private, and the word never leaves the phone as sound. delivery: 'spoken': the app speaks say, or plays audio_url (audio_delivery says which form it carries), so a purchase completes with no look at the screen; the word stays on the screen as well. voice.challenge takes the same option.

The choice is yours, and it has a cost. Spoken through the loudspeaker, a nearby recorder hears the fresh word before the person has said it. Over headphones or the earpiece it stays with them. The app half reads the audio route (this package cannot) and names it on the streamed answer as channel; the @agent-cards/voice README says how the reference app reads it.

A bot with no screen

A messaging bot or an agent with no screen in front of the person sends link.url or purchase.approval_url as a text itself; the person opens it on their phone. Leave return_url out on such a connect: the Vault ends on "Connected", the link polls (grant: 'poll'), and voice.awaitConnected(link.session_id) reads the result.

Languages

A language is a BCP 47 tag, and the list Agentcard speaks is data: await agentcard.voice.languages() reads it ([{ tag: 'en', name: 'English' }, { tag: 'es', name: 'Español' }]), and every call that names a lang checks the tag against it first, refusing one not on the list with unsupported_language before anything is sent. A language added on Agentcard's side needs no new release of this package.

Reading, without waiting

voice.awaitResult, voice.awaitConnected and voice.actions.awaitDone wait. For one read with no wait, from a handler that answers a poll: voice.authorizations.get(id), voice.sessions.get(session_id), voice.actions.get(session_id). Every read waits out the API's slow_down for you. Passing timeout_ms: 0 to an await verb is not a read; use the read verbs.

Webhooks

Agentcard signs every delivery. Verify against the raw bytes and branch on type:

import { verifyWebhook, isCheckoutAuthorizationEvent } from '@agent-cards/sdk';

const event = await verifyWebhook(rawBody, req.headers['agentcard-signature'], [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET_PREVIOUS]);
if (isCheckoutAuthorizationEvent(event) && event.type === 'checkout_authorization.approved') markPaid(event.data.id);

More than one secret is accepted, so a secret rotates without a missed event; a stale timestamp, a malformed header and a wrong signature each throw an AgentcardError with its own code. The event unions are CheckoutAuthorizationEvent (checkout_authorization.*) and VaultEvent (vault.*); an event of a type this release does not name arrives as OtherEvent, so branch on type and read data yourself.

Complete a purchase you already approved

agentcard.buy is Agentcard's Purchase API from the same client: it finds the merchant, builds the cart and places the order for a person you connected. When the person already approved the purchase by voice, the confirm names that approval, and the API places against it and asks them for nothing a second time. Same agentcard and user_id as the hello world.

const purchase = await agentcard.voice.authorizations.create({ user_id, merchant: 'Coffee', amount: 650, currency: 'usd', request: 'sandbox' });
const result = await agentcard.voice.awaitResult(purchase.id);                                          // { status: 'approved', approved_with: 'voice' }
const turn = await agentcard.buy.ask({ user_id, ask: 'a flat white from Coffee to my desk' });          // { status: 'needs_input', cart: { hash, totalCents, items } }
const placed = await agentcard.buy.confirm({ user_id, conversation_id: turn.conversation_id, confirm: turn.cart.hash, authorization_id: purchase.id });
const state = await agentcard.buy.conversation({ user_id, conversation_id: turn.conversation_id });     // { turn_in_progress: false, orders: [{ order_id }] }
if (placed.status !== 'order_placed') console.log(placed.status, placed.decline_code);                  // 'declined' names why; 'needs_input' is a question, not a failure

Every call names user_id; a person not connected to your client is refused with user_not_connected. buy.ask answers one turn: status is needs_input (a question, or a cart waiting for its confirm), order_placed, partially_placed (several carts, some placed; each one's outcome is in placements) or declined (with decline_code). Read turn.cart.hash and never the prose; a cart that changed since its hash was issued refuses the confirm with cart_changed, and the fresh carts ride on the error. buy.confirm with authorization_id places against the approval the person gave: one that is not approved, is for another purchase, or already paid an order is refused with authorization_not_approved, authorization_mismatch or authorization_spent, and nothing is charged; the answer carries authorization_id back. Without it the confirm runs as before, with payment_source: 'vault' for the person's own card. A turn runs against live merchants, so this call waits up to 130 seconds and is sent once, never retried; a turn still running past the API's own budget answers error_code: 'turn_timeout' with the carts it had shown, and buy.conversation reads whether it is done (turn_in_progress) and every order it placed. Refusals are a BuyError, an AgentcardError with the API's code; isBuyDeclined(turn) narrows a declined answer. buy.test.mjs runs this block against a stub on every test run.

Approve a purchase by voice inside your app

When your app holds the person, the confirm asks for their approval by voice instead of an approval page: approval: 'voice' with payment_source: 'vault'. The API mints the purchase's own approval and answers status: 'needs_approval' with its authorization_id; your server mints the key word on that id with voice.challengeForHost, exactly as for a purchase you created yourself, the app runs it, and the same confirm sent again places the order. buy.approveByVoice does the whole dance: one confirm, your challenge on the id, one retry. app.approve(bundle) below stands for your app taking the bundle to VoiceHost.approve.

const turn = await agentcard.buy.ask({ user_id, ask: 'a flat white from Coffee to my desk' });          // { status: 'needs_input', cart: { hash } }
const placed = await agentcard.buy.approveByVoice({ user_id, conversation_id: turn.conversation_id, confirm: turn.cart.hash,
  challenge: async (id) => { await app.approve(await agentcard.voice.challengeForHost(id)); await agentcard.voice.awaitResult(id); },
});                                                                                                     // 'order_placed', 'declined', or 'voice_execution_unsupported'
if (placed.status === 'declined') console.log(placed.decline_code);                                    // 'voice_declined' | 'approval_expired'
if (placed.status === 'voice_execution_unsupported') console.log(placed.error.message);               // take your own path; nothing was charged

challenge is called once, with the authorization_id the API minted and the needs_approval turn it rode on; resolve it when the verdict is in (voice.awaitResult, or your app's own word), and the retry follows. The answer is the final turn: order_placed with order_id and the authorization_id it was placed against; declined with decline_code: 'voice_declined' when the person said no, or 'approval_expired' when the approval ran out first; needs_approval again when the verdict was still out, in which case a later buy.confirm with the same hash and approval: 'voice' completes it. A store whose connector cannot place under a voice verdict answers 409 voice_execution_unsupported; approveByVoice returns it as status: 'voice_execution_unsupported' with the BuyError in error, and buy.confirm throws it, so your fallback is a branch and not a catch. A challenge that rejects is thrown as it came and nothing is placed. isBuyNeedsApproval(turn) and isBuyVoiceUnsupported(turn) narrow the two new shapes. Without approval the confirm is what it was: the page on approval_url.

Browser purchases

Your agent drives checkout normally. When the page tries to send the card, the SDK pauses that one request, the cardholder approves on their device, the device supplies the card and calls the processor, and the page gets the answer to replay.

your agent ──drives──> merchant checkout
                            │ card request
                            ▼
                    [ paused by this SDK ]
                            │  template only, placeholder card
                            ▼
                  Agentcard ──notify──> cardholder's device
                                             │ decrypts card locally
                                             ▼
                                     processor's card vault
                            ┌────────token────────┘
                            ▼
                    [ request resumed ]  ──> order completes
const agentcard = new Agentcard({ clientId, clientSecret });
await agentcard.checkout.syncRegistry();              // the API's current recognisers; a failed read keeps the built-ins
await agentcard.checkout.attachToCdp(cdp, pageSessionId, {
  user: 'usr_123',                 // whose card should pay
  merchant: 'vanman.shop',
  amount: 583,                     // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
  currency: 'usd',
  onApprovalUrl: (url) => sendToUser(url),   // SMS, push, email, iMessage: your call
});

Playwright: agentcard.checkout.attachToPlaywright(page, { user, merchant, amount, currency }). The named exports attachToCdp(cdp, id, { vault: agentcard, … }) and attachToPlaywright(page, { vault: agentcard, … }) are the same calls.

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.

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:

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".

Attach with the amount as a number in the smallest unit, and the SDK accepts it:

await agentcard.checkout.attachToCdp(cdp, pageSessionId, { user: 'usr_123', merchant: 'vanman.shop', amount: 2667, currency: 'usd' });

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.

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.

Pause only, and let your server create

attachToCdp(…, { pauseOnly: true, onPaused }) pauses the card request without creating the purchase and hands you { request, recognizer, page_origin, controller }. Create it with voice.authorizations.create as on any other purchase, read the result, and controller.resume(result) replays the processor's answer into the page. recognizer names the processor that claimed the request (stripe), and so does the card_request_paused event's detail, so nothing has to re-match the URL.

Recognisers and request builders

Both are registries the SDK ships and you extend at runtime, so a processor or a store Agentcard does not know yet needs no release.

agentcard.checkout.recognizers.list();                                  // the API's list (after syncRegistry) or the built-ins, plus yours
agentcard.checkout.recognizers.register({ psp: 'acme', match: /pay\.acme\.test\/tokens/i, hosts: ['^pay\\.acme\\.test$'], encoding: 'json' });
agentcard.checkout.recognizers.find('https://pay.acme.test/tokens');   // { psp: 'acme', … } | null

agentcard.checkout.requests.list();                                     // stripe/payment_intent_confirm, stripe/card_token, stripe/payment_method, plus yours
agentcard.checkout.requests.build({ processor: 'stripe', kind: 'payment_intent_confirm', origin: 'https://yourapp.example', intent: { id, client_secret, publishable_key } });
agentcard.checkout.requests.register({ processor: 'acme', kind: 'pay', build: ({ order }) => ({ url, method: 'POST', headers, body }) });

The three shipped builders are the Stripe shapes Agentcard completes from a direct create today: confirming a PaymentIntent your server created, tokenizing a card for your server to charge, and creating a PaymentMethod for your server to confirm with. A Braintree or Mercado Pago purchase pauses in a browser (attachToCdp) or runs as a prepared checkout (checkout.prepareCheckout); neither has a direct-create builder yet. stripePaymentIntentConfirm(origin, intent) stays as a named export for one release.

Moving from @agent-cards/checkout 0.x

@agent-cards/checkout is this package's older name and re-exports it for one release. @agent-cards/voice is the phone package, not an alias of this one. VaultClient constructs the same client as Agentcard; every top-level checkout verb (syncRegistry, authorize, isCardRequest, …) is also under agentcard.checkout. The person's connection verbs (connect, completeConnect, exchange, awaitConnected, disconnect, sessions.get, people.get) are the voice module's. voice.authorizations.create and voice.appKeys.register now require user_id: there is no default from an earlier connect. lang is a Language tag (a string), not a union. The phone package is @agent-cards/voice.

The paused request

A purchase's status is one of awaiting_approval, approved, declined (the person said no, a rule or the processor refused: terminal_reason says which), cancelled (withdrawn before a decision, by you or by the checkout runtime), expired, or, from awaitResult alone, timeout at your deadline. voice.authorizations.cancel(id, { reason }) withdraws a purchase the person has not decided yet, in your words (person_closed_sheet, person_changed_mind, business_cancelled). checkout_origin on the create (or the paused request's origin header) is what an auto-approval rule needs to take the purchase; without it the create answers execution_reason: 'unverified_origin'. On a finished purchase awaitResult and authorizations.get carry settlement: whether the charge settled, as Agentcard read it back from the processor (status: 'settled' | 'not_settled' | 'unknown', final, settled_amount, message).

The connect link is an OAuth authorization grant: voice.connect mints state and a PKCE challenge, the sheet's return link carries code and state, and voice.completeConnect(url) exchanges it once, within 60 seconds, refusing a state it did not mint before any call. When another server instance minted the link, pass the code_verifier and expected_state that connect returned there. A mint with no return link polls (grant: 'poll', then awaitConnected). readReturnLink(url) reads a return link's code, state and voice_reset. voice.connect({ landing: 'voice_options' }) mints a link that lands the signed-in person on the Vault's Voice options page (the voice key, its reset, the language, the passkey it rests on) for an app's own settings row; it collects nothing and polls.

Credentials

Use your OAuth client credentials, not an sk_ API key: those are retired, and the checkout endpoints reject them (client_credentials_required) because an authorization is bound to the confidential client that created it. The SDK does the client_credentials exchange for you, caches the token, and refreshes it once on a 401. Create a client from the dashboard Credentials page or with agent-cards-admin oauth-clients create.

Why you need the SDK and not just Fetch.enable

Card fields render in cross-origin iframes, which are separate CDP targets. Enabling Fetch on the page session never sees the tokenization request. You need recursive Target.setAutoAttach({ flatten: true }) on every nested target, then Fetch.enable on each, then Runtime.runIfWaitingForDebugger to unpause them. That, plus which headers a merchant requires you to replay verbatim, is what this package encapsulates.

What runs where

| | Sees the real card | |---|---| | Your agent / browser | no, only a dummy PAN and a token | | Agentcard servers | no, a request template and a token | | Cardholder's device | yes, decrypts locally, calls the merchant directly |

On a reviewed merchant's own card endpoint (mode card_endpoint, below) the device sends the card to the merchant through Agentcard's checkout relay, which carries end-to-end TLS it cannot read. Agentcard's servers hold the paused request's session and the merchant's answer encrypted, for minutes, and hand each over once.

Because your process only ever handles a dummy card and an opaque token, this integration is designed to keep you out of PCI scope. Get your own QSA's read before you put that in writing.

Supported processors

Coverage is specific to the processor request format, merchant setup, browser transport and follow-up flow. A recognized endpoint is not proof that every store using that processor completes checkout.

| Processor | Status | |---|---| | Shopify | supported, verified end to end | | Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as amount + currency must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use | | Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK 0.5.0 confirmed merchant fulfillment and SDK completed using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. | | Checkout.com | supported | | 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. | | 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 | | PayPal Hosted Fields (the legacy card fields of PayPal's JS SDK; Planet Golf UK) | not supported: no Agentcard payment takes over the card these fields send to cors.api.paypal.com. The SDK fails that request for every company, so PayPal's payment confirm never receives the dummy card, and the controller reads unsupported with reason unsupported_paypal_hosted_fields and holds the attachment until you attach again; a PayPal wallet payment continues untouched | | CyberSource Secure Acceptance Silent Order POST (the merchant's own card form posted to CyberSource; Arrow Electronics) | dark launch: prepared checkout only, for an org Agentcard enabled. After syncRegistry(), for such an org, the SDK pauses the token create a merchant's form posts into a hidden iframe (/silent/embedded/token/create on secureacceptance.cybersource.com, or testsecureacceptance.cybersource.com in CyberSource's test environment), the cardholder's device sends the card to CyberSource, and the iframe gets CyberSource's own reply page. See Pay a CyberSource Secure Acceptance checkout. For every other company, and for every other card post to a /silent/ path on those hosts or on secureacceptance.in.cybersource.com (India), no Agentcard payment takes the card over, and the SDK stops the post so CyberSource never receives the dummy card: a form the page submits as a page load, in the page or in a frame, ends on the page that says Agentcard stopped the payment, and a fetch or an XHR fails. The controller reads unsupported with reason unsupported_cybersource_secure_acceptance and holds the attachment until you attach again; a post with no card_number, such as a payment with a stored payment_token, continues untouched. The SDK does not stop Hosted Checkout, where your agent types the card on CyberSource's own page | | 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. Cinemark and Dick's Sporting Goods' own storefront take payments for a company Agentcard turned them on for; every other reviewed merchant is in observe, so its 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 | | 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 | | 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 | | 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 | | 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 | | 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 its card request but not 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 | | Gravy (Gr4vy) Secure Fields | dark launch: for an org Agentcard has enabled, after vault.syncRegistry(), which declares gravy_session_fields_put; for any other org the SDK leaves Gravy's requests alone and the request continues to Gravy untouched. The card request is Secure Fields' update of a checkout session: a JSON PUT to /checkout/sessions/<session id>/fields on the merchant's own host api.<merchant>.gr4vy.app (sandbox: api.sandbox.<merchant>.gr4vy.app), sent from Gravy's hidden controller frame and carrying payment_method.number, payment_method.expiration_date as one MM/YY value and payment_method.security_code. The SDK pauses that PUT, never a POST to the same URL; the cardholder's device sends the card as the same PUT, and the frame receives Gravy's answer, a 204 with no body. The request names no amount, so pass amount and currency. 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 | | GMO Payment Gateway | dark launch (mode token): only for an org Agentcard has enabled; see Pay a GMO Payment Gateway checkout | | 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 | | 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 | | Reviewed merchant card endpoints: AT&T, Italo, Universal Orlando, Target Optical, Opus Virtual Offices, Delta, Hilton, IHG, American Airlines, EconomyBookings, The Verb Hotel, RunSignUp, Mindbody, LookFantastic, Lexicon (FastSpring's popup checkout), and Hyatt, LifeMiles, Southwest Airlines and United Airlines, whose page encrypts the card under the merchant's own key, and the card forms Athens-Clarke County's MyRec page, Los Angeles and San Francisco on Conduent eTIMS, the D-EDGE card frame and NIC Common Checkout (NJMCdirect and other state agencies) submit as a page load | recognized in mode card_endpoint, every one in observe: the adapters pause each merchant's own card request and refuse it as unsupported_checkout before any authorization, so the dummy card never reaches the merchant, and a request to the same URL with no card in it continues untouched. A profile takes payments only after a reviewed partner HAR, a reviewed release of this SDK and the API, and per-org enablement | | 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). 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 | | 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 | | 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 | | 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 | | Primer (Web SDK v2 hosted fields; Printful, GetYourGuide) | dark launch (mode token): only for an org Agentcard has enabled; see Pay a Primer checkout | | Payrails web SDK (Blacklane, FlixBus) | dark launch (mode device_encrypt): only for an org Agentcard has enabled, and only the card form's single-use save and its single-use payment until card-on-file consent exists; see Pay a Payrails checkout | | 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 or www.convergepay.com (api.demo.convergepay.com in Converge's demo environment); no amount needed. See Pay an Elavon Converge payment page | | Moneris Hosted Tokenization (Moneris's card frame; Raisin donation pages) | dark launch: after vault.syncRegistry(), and only for an org Agentcard has enabled, the SDK pauses the frame's post to /HPPtoken/request.php on www3.moneris.com or gateway.moneris.com (esqa.moneris.com for a test row) when it carries the card number, expiry and CVD, always with the cardholder's approval. A live payment needs a merchant Agentcard reviewed: the profile id the frame names must be one pinned for the checkout page's origin (Jack.org's Raisin page today), or the create answers unsupported_checkout. A frame that leaves the expiry or CVD to the merchant's page, or a post to any other request.php URL, fails in the browser with a blocked event, request_without_card. The cardholder's device sends the card through the checkout relay, and the page gets Moneris's temporary token, which the merchant charges from its own server: pass amount and currency, the total the cardholder approves. Moneris Checkout (/chkt/, /chktv2/) is not supported | | WSPay Components (WSPay's build of Monri Components) | dark launch: the card frame's confirm of a new card, 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, as Valamar's does on every booking, is paid only as a hotel's EUR 1.00 card guarantee with a card the cardholder stored. 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 | | Shiji STS (MyCheck Wallet V3; Wyndham's booking pages) | dark launch, mode cse under a pinned key, and only for an org Agentcard has enabled. The card token a hotel booking mints guarantees the stay, so the cardholder approves with a card they stored, under a line that says the hotel can keep it on file. See Shiji STS | | Amadeus Checkout (Air France, KLM) | dark: only for an org Agentcard enabled, and only through prepare({ psp: 'amadeus_checkout', cardId }). The Checkout Web SDK's card add pauses; every other action on its URL continues. Air France and KLM keep the expiry in their own form, so the agent types the stored card's real expiry there and the checkout pays only with that card. Every Data Collection Page store is refused as amadeus_dcp_field_config_unreviewed. No live purchase yet. See "Prepare an Amadeus Checkout card step" |

The recognizer list, and the status of each reviewed Adyen merchant profile on your client, are fetched from the API at runtime (agentcard.checkout.syncRegistry()), so new processors work without you shipping a release. attachToCdp derives the Fetch.enable url patterns from that same list rather than a constant, which is why the sync call belongs before the attach. agentcard.checkout.cardUrlPatterns() returns those patterns if you arm a CDP connection yourself. Call GET /v2/checkout/recognizers?modes=token,cse,hosted_form,device_encrypt&capabilities=fiserv_card_capture,gmo_shared_pk,capture_context,juspay_txns.v1,datatrans_secure_fields_tokenize,authorize_net_dpm_form,pelecard_transaction,aci_split_card.v1,creditguard_pps_process,primer_payment_instruments,payrails_device_encrypt,converge_hpp_process,worldpay_eprotect_jsonp,wspay_confirm,shiji_sts_pinned_key,amadeus_checkout_actions.v1,worldline_mycheckout_form,verifone_card_capture,ekashu_hosted_payment_page,cybersource_sa_token_create,gravy_session_fields_put for the list this build syncs. The API serves Fiserv's card capture only to a build that lists fiserv_card_capture, and only for a company Agentcard turned Fiserv on for. An older SDK never pauses a Fiserv request it could not finish, and no SDK pauses one for a company Fiserv is off for. The API serves Juspay's card flow the same way, behind juspay_txns.v1, Datatrans's Secure Fields card post behind datatrans_secure_fields_tokenize, the Authorize.Net DPM or SIM form behind authorize_net_dpm_form, Pelecard's card submission behind pelecard_transaction, ACI's split card behind aci_split_card.v1, CreditGuard's hosted page submit behind creditguard_pps_process, Primer's card tokenization behind primer_payment_instruments, WSPay's card confirm behind wspay_confirm, Shiji STS's card leg behind shiji_sts_pinned_key, Amadeus Checkout's card add behind amadeus_checkout_actions.v1, Worldline MyCheckout's card form behind worldline_mycheckout_form, and Payrails' card save behind payrails_device_encrypt to a build that also asks for device_encrypt. The same holds for the Verifone profiles and verifone_card_capture. NMI's hosted payment page (ekashu_hosted_payment_page) is served on the same terms: this build pauses the merchant's entry form to live.ekashu.com for the cardholder's device and fails every other post your browser makes to NMI, so NMI never sees the card your agent typed. CyberSource Secure Acceptance's token create (cybersource_sa_token_create) is served on the same terms too; every other company keeps the stop every build applies to a card posted to Secure Acceptance. So is Gravy's session fields PUT (gravy_session_fields_put): this build pauses that PUT and only that method, and for every other company the request continues to Gravy untouched.

Modes

Each recognizer carries a mode (absent means token), and every authorization carries the mode it was handled in. The adapters do the right thing for each mode; the difference matters if you drive authorize() yourself. ReplayResponse is a union, so branch on mode.

  • token (every processor but Adyen and Fiserv). The cardholder's device calls the processor and reports its answer; authorize() resolves with status, headers and body to fulfill the paused request with. The browser checks a fulfilled answer exactly as it checks a real one, so when the page called the processor cross-origin (Stripe always does: Checkout on checkout.stripe.com and Elements in the js.stripe.com frame both fetch api.stripe.com) the answer must carry access-control-allow-origin for the request's own Origin, or the page's fetch rejects and the checkout reports a connection error even though the cardholder approved. The adapters add those headers (corsHeadersFor + withCorsHeaders, exported for a runtime that fulfills by hand, and corsDecision when you also want the reason) and report the decision on the authorized event as cors: echoed, same_origin, or none (no usable Origin on the request, so the page could not read the answer). Only what a browser serializes is echoed: one canonical http(s) origin, or the opaque null. Shopify's card iframe posts to its own origin, so it never needed them.

  • cse (Adyen and Fiserv). Adyen's own page SDK encrypts the card before the request leaves the browser, so the paused body carries ciphertext. The cardholder's device produces the same ciphertext under the merchant's Adyen public key (fetched by Agentcard from Adyen's host when the request is parked) and authorize() resolves with substitutions: { encoding: 'json', at, fields, remove }. Write them into the paused body with substituteEncryptedFields(body, substitutions) and CONTINUE the request from the same browser (Fetch.continueRequest with the rewritten postData, or Playwright's route.continue({ postData })): its session data, risk data and cookies must stay its own. Only the four encrypted fields change, and the siblings named in remove are dropped (Adyen's brand, which adyen-web derived from the dummy digits the agent typed: left in place it names the wrong card and Adyen refuses the mismatch; absent, Adyen reads the brand off the card it decrypts). A body that lacks the fields throws SubstitutionError, which is not terminal. Adyen answers the browser, so charged_kind is null on the approval and the merchant's order state is the outcome to poll. A cse replay with kind: 'merchant_hosted' is an Adyen merchant's own endpoint instead: its substitutions name the profile, a JSON path ([] is the body root) and the merchant's field names, and substituteMerchantHostedBody(body, replay) writes it after checking the body against bodySha256 (see "Adyen merchants that take the card on their own server"). A Fiserv card capture (a prepared checkout only) comes back with its envelope's four members at source.encryptionData: write them with substituteFiservEnvelope(body, substitutions, url), url being the paused capture's own, never substituteEncryptedFields, and continue the request the same way. substituteFiservEnvelope throws, writing nothing, when the body is not the paused capture or the envelope names another key than the one Agentcard pinned for that capture, and for a capture under a credentials session's key when url is absent or is not the capture endpoint. The API serves a Fiserv envelope only in the moments after the approval; a later read raises PaymentOutcomeUnknownError with reason cse_substitutions_expired.

  • hosted_form (Tranzila, Authorize.Net DPM and SIM). The processor's hosted card form, or the merchant's own card form that posts to the processor, submits the card as a form post, so the paused request is a navigation (the adapters arm Fetch.enable with no resource-type filter and attach the processor's iframe, which is how a Document request on direct.tranzila.com gets paused at all). The cardholder's device rebuilds that form with the real card and submits it itself; the processor answers the device, and authorize() resolves with { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', submittedAt } once the device reports the form left. This is not an approved payment. The stamp is the cardholder's device attesting that the form left it; Agentcard holds no processor evidence on this mode and cannot obtain any, so the API finishes the authorization as submitted_on_device (never approved) and sends your server checkout_authorization.submitted (never .approved). Treat it as "the person paid, or tried to, on their own device" and confirm the order with the merchant before you count it. There is no response to replay: FULFIL the paused navigation with hostedFormSubmittedPage({ authorizationId, merchant, submittedAt }) (200, text/html, x-agentcard-checkout: submitted_on_device, a <meta name="agentcard-checkout"> and an inert JSON block saying "submitted on the cardholder's device, payment unverified, do not resubmit"), the way the adapters do. Do not abort it: an aborted navigation renders nothing, the iframe silently keeps its dummy-card form, and the agent's next move is to click Pay again. Do not fake the processor's result page either: this SDK does not know the outcome. The adapters emit submitted_on_device (not authorized), refuse a byte-identical re-post of the same form for 15 minutes (hostedFormRepeatQuietMs), and the API answers a regenerated one with 409 duplicate_submission, which the adapters quiet the way they quiet a decline (approvalCooldownMs): the page's immediate re-posts are refused without a round trip, and once the prior authorization is declined or expired the same form is a new question. Confirm the order with the merchant, which learns the outcome from the processor.

  • device_encrypt (Payrails). The page encrypts the card under a per-session key that the paused request does not carry: it came in the Payrails init payload the merchant's backend handed the page. The adapters read that payload from the page's own XHR and fetch answers and send its key with the create as request.payrails.encryption_public_key, and for a card-form payment its holderReference as request.payrails.holder_reference; a caller that runs its own interception passes them the same way, and a Payrails request without them throws before anything is created. The cardholder's device encrypts the real card under that key and sends the save or payment to Payrails itself, and authorize() resolves with { mode: 'device_encrypt', status, headers, body }: Payrails' 201 for a save, or for a payment its 202 naming the authorize action of that same payment (its own execution, on its own host), rebuilt from an allow-list, to fulfill the paused request with as in token. The ciphertext the device made never reaches your runtime. Any other answer throws PaymentOutcomeUnknownError (payrails_answer_unconfirmed), and autopilot never runs this mode. See Pay a Payrails checkout.

    Authorize.Net's Direct Post Method form (the merchant's own card form) and its SIM hosted form both post to /gateway/transact.dll, with the card in x_card_num, x_exp_date and x_card_code beside the merchant's signed fingerprint. Authorize.Net reads field names without regard to case, and so does the SDK: X_Card_Num is a card request too. The same URL without a card continues untouched: SIM's first step (x_show_form=PAYMENT_FORM, which loads the hosted form), an eCheck and Visa Checkout. The endpoint is the bare URL: a form whose action carries a query string or a fragment is not recognized and continues untouched, exactly as the API refuses it. A fingerprint that Authorize.Net would find stale (more than 40 minutes old at the create, or ahead of the API's clock) is refused for that form only: the page reloads with a fresh fingerprint and pays. Authorize.Net spends a fingerprint once, so never resubmit a form the cardholder's device sent.

  • card_endpoint (a reviewed merchant's own card endpoint). Some merchants post the typed card from their own checkout page straight to their own server, with no processor in between. Agentcard reviews each one and pins a profile in this SDK's copy of the payment core: the checkout page's origin, the exact origin, path and query of the card request, the session headers and cookies it needs, and the answer headers that may come back. The adapters arm every profile's endpoint (cardEndpointPatterns() lists the Fetch.enable patterns) and pause a request to exactly a profile's card request URL, sent with the profile's own method (a POST, or the PUT Target Optical's checkout sends), when it carries a card: anything at the profile's card number field, or a Luhn-valid number of 12 to 19 digits anywhere in the body, however it is written. A body the adapter cannot read counts as carrying one. A GET, a CORS preflight, another method, a look-alike URL, or a request to the same URL with no card in it (an IHG stay paid with points, a saved card, a room another SynXis hotel guarantees with a token) continues untouched. While this build's copy of a profile does not take payments (every profile is in observe today) a card request is refused as unsupported_checkout before any authorization exists. When the body is the request the profile reviewed, the controller reads unsupported with reason merchant_card_endpoint_not_enabled and the event names the profile; a card in any other body reads reason unrecognized_payment_endpoint, and the event names only the endpoint origin. LookFantastic's checkout takes the card in BR-DGE's card fields and then sends the masked card number BR-DGE returns in its order, so the adapters refuse that order with reason unrecognized_payment_endpoint. For a profile that takes payments, the adapter captures the request's session: the profile's own header names from the paused request, the endpoint's cookie jar for a cookie profile (Network.getCookies or context.cookies(), since the paused headers never carry it), and a Referer only when it is a page on the origin the request is sent from: the merchant's own card vault frame when the profile names one (EconomyBookings), else the checkout page. It sends them with the paused body, which holds only the dummy card, in the create's merchant_card_endpoint member, never in request, and checks them first with the same rules the API and the device run. The cardholder's device sends the card request to the merchant itself over Agentcard's checkout relay and reports the merchant's answer; authorize() resolves with { mode: 'card_endpoint', profile, status, headers, setCookies, body, withheld }. The adapters write each Set-Cookie into your browser host-only on the endpoint host and Secure, so none of them travels over plain http. A Set-Cookie whose Domain names a parent of the endpoint host that your browser already holds a cookie of that name on is written there instead, so it replaces that cookie rather than sitting beside it; one whose Domain the endpoint host is not inside is dropped, as a browser drops it. Then the adapters fulfill the paused request with the merchant's status, the profile's answer headers, the CORS headers a cross-origin page needs, and the body. When the device withheld the answer (it carried the card, or a compressed stream the device cannot read), the paused request gets a fixed 502 JSON error with code agentcard_answer_withheld, and the controller reads outcome_unknown with reason merchant_answer_withheld: the merchant has the card, so check the order. Every payment needs the cardholder's approval (no Autopilot, no prepare()), and the amount you pass is the only amount authority, so an attachment without one is refused. The approval never outlives its request: a card request the page gives up on retires the authorization. After the hand-off the attachment holds, so the page's retry of the card request, or a step-up that sends the card again, is refused until you reconcile the order.

    Lexicon sells through FastSpring's popup checkout, which needs four more rules. The card is typed into FastSpring's frame on the store's own host inside Lexicon's page, so the page a card request pauses on is that frame's document (the frame tree in CDP, request.frame() in Playwright). FastSpring posts PayPal and wallet payments to the same URL: a body whose paymentType names another method and that carries no card continues untouched, and the adapters emit card_endpoint_passed; one that names another method but carries a card is held like any card request. FastSpring's EBANX step, which reads the typed card back and posts it to /3ds/return-ebanx, is aborted in every status (card_endpoint_refused with reason card_resend, the controller reads unsupported with reason merchant_card_endpoint_resend_refused); the device withholds the answer that would start it, which the paused request receives as a fixed 502 with code agentcard_step_refused. And the SDK records FastSpring's own record of the order, read-only, from the popup document and each JSON answer its page reads the order from, a quantity or add-on change included. When the card request pauses it sends the newest one with the create, cut to the total with tax, the currency, test mode and the renewal marks; a total or currency other than your integer amount is an AmountMismatchError against the merchant's total, and a renewing order or a checkout in FastSpring's test mode is refused as facts_refused, both before any authorization. A request the popup sends on the same checkout after that record, other than one the SDK reads the order from or FastSpring's customer recognition, leaves the order unknown: the payment is refused as facts_unavailable, and a reload of the popup reads it again. The SDK keeps 32 of these requests. A checkout that sends more is refused the same way when one the SDK let go could be newer than the record it read, and for the rest of that checkout session once one went while still loading. Card endpoint events name the profile and the endpoint origin, never the request's URL, body or session: the URL carries the merchant's cart and booking ids.

    Some of these merchants' pages submit the card form as a page load rather than a fetch: the profile's navigation is top, or frame for a form inside an iframe, which is then judged by the iframe's own document. The same URL takes the page's other posts, so a post whose card fields are empty and that holds no card number (a Cancel button, another payment method) continues untouched in any status, and a card post a script sends is aborted, since no page waits on it. The reviewed page load pauses, its session carries only the cookie names the profile lists, and the merchant's answer comes back as a page load: its page, or a 301, 302 or 303 your browser follows in its own session with the merchant's cookies written first. A 307 or 308 would make the browser post the typed form again, so it is refused. Every stopped page load (a profile in observe, a post that is not the reviewed form, a declined approval, an approval already outstanding, a refused redirect, a withheld answer) is answered with a small HTML page instead of an aborted load. Its <meta name="agentcard-checkout">, its x-agentcard-checkout header and its JSON block (#agentcard-checkout) say one of three outcomes. stopped_before_send: nothing reached the merchant, and no other payment for this checkout is waiting. sent_answer_not_shown: the card reached the merchant, so check the payment with the merchant before paying again. payment_in_progress: a payment for this checkout is waiting for approval or may have reached the merchant, so check that payment and never send the form again. The page reads payment_in_progress while an approval is outstanding (the cardholder's device sends the card before it approves), after a payment this attachment handed over or cannot account for, and when the API refuses the create with 409 merchant_checkout_in_use, as it does on every later post of that attachment. A local deadline whose authorization the API confirms it retired before any processor request reads stopped_before_send: the PaymentOutcomeUnknownError then carries processorRequestStarted: false.

syncRegistry() asks the API for SUPPORTED_MODES only (token,cse,hosted_form,device_encrypt), so a processor whose flow this build cannot finish is never paused; the API serves hosted_form and device_encrypt entries only to callers that ask. syncRegistry() also sends SUPPORTED_CAPABILITIES (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), the dark-launch half of the same negotiation: the API serves a processor gated behind a capability only to a build that declares it, for a company Agentcard turned that processor on for, and the built-in list never holds one, so the SDK pauses it only after a sync. A registry entry naming a capability this build does not declare throws UnsupportedCapabilityError before creation. A runtime that calls GET /v2/checkout/recognizers itself without that parameter, such as a native provider integration, never receives the Fiserv, GMO, CyberSource Flex, Juspay, Datatrans, Authorize.Net DPM, Pelecard, Primer, Payrails, Worldpay eProtect, WSPay, Shiji STS or Amadeus recognizer. See "Prepare a Worldpay eProtect checkout (dark launch)" and "Pay a WSPay Components checkout" below. A registry mode this SDK cannot finish throws UnsupportedModeError before creation. An approval returned in an unexpected mode has an unknown outcome and holds the attachment for reconciliation. The authorized event's detail names the mode, the authorizationId and, for cse, the fields that were substituted; it never carries ciphertext. The submitted_on_device event's detail names the authorizationId, submittedAt and outcome: 'unverified'; it is not an authorized event and must not be counted as one. amountAuthority on every replay is stripe_payment_intent, hosted_form_sum (the form's own amount) or display_only.

Shiji STS (MyCheck Wallet V3)

Wyndham's booking pages (every brand on www.wyndhamhotels.com, La Quinta and Days Inn among them) take the card through MyCheck's wallet, whose Shiji proxy (sts.proxy.v2.1.js) encrypts the PAN and the MM/YY expiry with RSA PKCS#1 v1.5 under a key Shiji hardcodes and posts {accessToken, pan, expires} to /v1/token/getTokenId on iframe.vault.eu.prod.shijipayment.com (sandbox: iframe.vault.eu.uat.shijipayment.com), then the CVV the same way to /v1/token/getCvvId under the merchant's client_key. Wyndham's wallet sends no keySequence on getTokenId, and the API admits that shape only from Wyndham's booking page. The processor ships dark: syncRegistry() declares shiji_sts_pinned_key (SUPPORTED_CAPABILITIES), and the API serves the recognizer only to an org Agentcard has enabled for it. Every other org's SDK recognizes neither URL, and the page's requests go through untouched, as before.

For an enabled org, both adapters judge each POST to those two URLs before anything pauses:

  • A getTokenId in the proxy's own shape is the one prompt. The cardholder's device encrypts the real card under the key Agentcard pins (never a key from the page), authorize() resolves with substitutions: { encoding: 'json', at: '', fields: { pan, expires, cvv } }, and the adapter writes pan and expires over the page's own at the body root and continues the request from the same browser. The authorized event names fields: ['pan', 'expires']. The ciphertext goes only into the request the approval was created from, byte for byte. If the page gives up on that request while the cardholder decides, the approval is cancelled (outcome_unknown, reason merchant_request_aborted), as for a hosted form: no later getTokenId is answered from it. Shiji uses one key for every merchant, so a request of the same shape could name another merchant's accessToken and keySequence, and the proxy's own request never times out.
  • The hotel keeps the card token to guarantee the stay. The approval page says the merchant can keep the card on file and charge it again later without another approval, and it offers only a card the cardholder stored: the API refuses a card a company added (409 card_not_stored), at the create when the runtime names one and at the approval. If Agentcard turns the card-on-file rule off, every such getTokenId is refused before any create or prompt: `unsupported