Skip to main content
A purchase needs a link at three moments. The shopper approves the purchase on an approval page, a new shopper adds a card on a Vault page, and a shopper signs in to a store’s own account on its login page before Agentcard can buy there. Your app delivers each link as one clear call to action, and the shopper finishes the step outside the chat. Your model never touches a link. A tool runner puts each tool answer straight into the model’s context, and a model that holds a link writes it into the chat. So the tool hands each link to your code, and the model reads only link_sent: true. Pass sendLink to buy.turn. The tool hands it each link and waits for it, so the shopper holds the link before the model answers.
chat.ts
Each link’s kind says which step it is: expiresAt is an ISO 8601 timestamp, or null when Agentcard did not say. Send each url exactly as the link carries it, alone in its own message or behind one button. A Vault or approval link with anything appended to it fails verification and opens the Vault’s sign-in page instead of the step. The model’s description tells it never to write a link and to name each link by what it is for: the link to add a card, to sign in to the store, or to approve the payment. When the shopper asks for a link again, the model calls the tool, and the tool sends it again. In SMS or iMessage, send the url alone in its own message after the sentence that asks for it. iMessage shows a URL that stands alone in a message as a preview card, and it mangles one with words attached. With onItem on the turn, the turn holds each step link until the message that asks for it went out, then sends the link right after it. The shopper reads “add your card to place the order” and then taps. A held link also goes out when the turn’s next call starts, or after 20 seconds, so a model that writes nothing still sends it. “Answering each shopper message in its own turn” shows onItem. Without sendLink, result.links still carries each Vault, approval, and store sign-in link for your code, and the model still never reads one. Read them from turn.links or from the onResult callback.

Open each step in a pop-up

Each step link carries a stable id, such as vault_1x9k2qd. It is the same every time the same link is handed over, on your server and in the browser, and it has no part of the URL in it, so it is safe as a DOM key or in a log. actionLinkId(link) reads the id from a link that has none. actionWindows from @agent-cards/commerce-sdk/browser opens a step in a pop-up window centered over the chat, so the shopper never leaves it, and closes the pop-up the moment the message that ends the step arrives. The browser entry imports no Node module, so any bundler takes it.
chat-ui.ts
settle closes each open pop-up whose step the message ends and returns [{ step, outcome }]. A pop-up the shopper closes before the step ends reaches onClosed, checked every 500 milliseconds. The step is still open, so keep its link on screen, and a tap opens it again. A tap on a step whose pop-up is open brings that window to the front instead of opening a second one. The pop-up is 480 by 760 pixels by default, kept inside the screen. Pass width and height to change it. A phone’s browser opens the pop-up as a tab, and settle closes that tab the same way. dispose() stops watching and leaves every pop-up open, so a chat that remounts never shuts the page where a shopper is typing a card.

Know which message ends which step

Each step ends with a message from Agentcard. actionLinkOutcome(link, message) returns how a message ended the step, or null when it left the step open. ACTION_LINK_ENDINGS holds the whole table: A reminder, such as card_link_unfinished or approval_expiring, leaves the step open. An approval is completed once the shopper approved, even when the order then failed or the payment was refused, because the approval page has nothing more to do. The message tells the shopper what happened. A message whose facts name another link of the same kind (facts.approvalUrl or facts.cardLinkUrl) leaves this step open. So does a message about another store: DoorDash’s store_connected never ends a Locale sign-in the shopper has open beside it.

Handle a failed send

A sendLink that throws on a Vault or approval link makes the call throw, and the model never reads that a link went out. Catch the error, tell the shopper something went wrong, and let them ask again. A link your sendLink throws on stays out of turn.links.

Check it in the sandbox

  1. Send “two packs of AA batteries from Amazon” as a test shopper who has no card. Check that your app receives a vault link and that the model’s reply says a link is waiting, without containing it.
  2. Ask again. Check that you receive the same link, because Agentcard returns the link it already opened while it has time left.
  3. Open the link and add a card with the code 111111. Check that the pop-up closes when card_added arrives.
  4. Close the pop-up before you finish. Check that onClosed fires and the link stays on screen.
  5. Search the model’s replies for https://. Check that none is there.
The sandbox stops before payment, so a confirm in the sandbox ends declined and places no order. “Approving a purchase” shows how to check the approval link with live credentials.