# Card Close (Beta) Source: https://docs.henrylabs.ai/api-reference/card/card-close-beta /v1/api-reference/openapi.documented.json post /card/{cardToken}/close **Beta.** Card issuance is in beta and enabled per account. Contact support@henrylabs.ai to request access; until then this endpoint returns `403`. Close an issued card. Final: a closed card declines everything. Within about 15 minutes of closing, the card settles: the buyer's card is refunded whatever the issued card didn't spend, and its cap stops counting against your app's open-card limit. Closing is optional — a card you leave alone settles when it expires — but closing once a purchase has fully completed refunds the buyer sooner. Don't close a card the merchant may still capture on (some charge when they ship). Idempotent. # Card Get (Beta) Source: https://docs.henrylabs.ai/api-reference/card/card-get-beta /v1/api-reference/openapi.documented.json get /card/{cardToken} **Beta.** Card issuance is in beta and enabled per account. Contact support@henrylabs.ai to request access; until then this endpoint returns `403`. Read an issued card: its cap, expiry, live state at the issuer, and what has been charged to it so far. Only cards issued to your app via `POST /card/issue` are readable — a tokenized customer card 404s. # Card Issue (Beta) Source: https://docs.henrylabs.ai/api-reference/card/card-issue-beta /v1/api-reference/openapi.documented.json post /card/issue **Beta.** Card issuance is in beta and enabled per account. Contact support@henrylabs.ai to request access; until then this endpoint returns `403`. Issue a virtual card for one purchase, paid for by your buyer's card. Henry Labs charges the buyer's tokenized card (`fundingCardToken`) for the card's cap — off-session, using the payment method saved when the card was tokenized, so no CVC is needed — then issues a card capped at that amount and locked to the first merchant that charges it. The buyer's statement shows `HENRY LABS* ` when you pass `merchantHost`. The response is a `cardToken` you pass to `cart.checkout.purchase` exactly like a tokenized card. Read it back with `GET /card/{cardToken}` and close it with `POST /card/{cardToken}/close`; when it closes or expires, the buyer is refunded whatever it didn't spend. Send an `Idempotency-Key` header: retries with the same key return the same card and never charge twice. Your app has a per-card maximum and an open-card limit: the summed caps of cards that haven't settled. # Card Reveal (Beta) Source: https://docs.henrylabs.ai/api-reference/card/card-reveal-beta /v1/api-reference/openapi.documented.json post /card/{cardToken}/reveal **Beta.** Card reveal is in beta and enabled per account, separately from card issuance. Contact support@henrylabs.ai to request access; until then this endpoint returns `403`. Return the card number, expiry and security code of a card issued to your app, for checkouts you run yourself rather than through `cart.checkout.purchase`. Only issued cards can be revealed — a tokenized customer card never can — and only while the card is open and unexpired. # Card Tokenize Source: https://docs.henrylabs.ai/api-reference/card/card-tokenize /v1/api-reference/openapi.documented.json post /card/tokenize Tokenize raw card details into a card token. **PCI scope warning:** This endpoint accepts the raw PAN, CVV, and expiry directly on your server. Sending unencrypted cardholder data through your backend brings your systems into PCI DSS scope (typically SAQ D). To stay out of scope, collect card details with the [Card Element](https://docs.henrylabs.ai/v1/sdk/client/elements/card-element) instead: it captures and tokenizes the card in a secure iframe, so raw card data never touches your servers, and returns the same `cardToken`. # Card Update Source: https://docs.henrylabs.ai/api-reference/card/card-update /v1/api-reference/openapi.documented.json post /card/{cardToken}/cvv Refresh the CVV on an already-stored card without re-sending the card number or expiry. Card verification values cannot be stored, so the CVV captured at tokenization expires while the rest of the card stays valid. Charging a stored card after that point requires the buyer to re-enter their CVV. This endpoint applies that value to the existing card, and the `cardToken` continues to reference the same card. **PCI scope warning:** this endpoint accepts the raw CVV directly on your server, which brings your systems into PCI DSS scope. To stay out of scope, collect it with the client SDK's CVC element instead, which captures the value inside a Henry Labs-hosted iframe so it never reaches your backend. # Cart Add Item Source: https://docs.henrylabs.ai/api-reference/cart/cart-add-item /v1/api-reference/openapi.documented.json post /cart/{cartId}/item Add an item to an existing cart. # Cart Create Source: https://docs.henrylabs.ai/api-reference/cart/cart-create /v1/api-reference/openapi.documented.json post /cart Create a cart with one or more items and get a checkout URL # Cart Delete Source: https://docs.henrylabs.ai/api-reference/cart/cart-delete /v1/api-reference/openapi.documented.json delete /cart/{cartId} Delete a cart by its unique identifier # Cart Details Source: https://docs.henrylabs.ai/api-reference/cart/cart-details /v1/api-reference/openapi.documented.json post /cart/{cartId}/details Retrieve detailed information about a cart. Requests are async by default, or use mode=sync to wait up to 30 seconds for completion. # Cart Details Status Source: https://docs.henrylabs.ai/api-reference/cart/cart-details-status /v1/api-reference/openapi.documented.json get /cart/checkout/status Check the status of a cart details retrieval job and get results when ready. # Cart Fetch Source: https://docs.henrylabs.ai/api-reference/cart/cart-fetch /v1/api-reference/openapi.documented.json post /cart/{cartId} Fetch a single cart by ID and optionally prefill buyer details in the returned checkout URL. # Cart List Source: https://docs.henrylabs.ai/api-reference/cart/cart-list /v1/api-reference/openapi.documented.json get /cart List carts with optional filtering by cart ID or tags. # Cart Purchase Source: https://docs.henrylabs.ai/api-reference/cart/cart-purchase /v1/api-reference/openapi.documented.json post /cart/{cartId}/purchase Initiate the purchase process for a cart. Requests are async by default, or use mode=sync to wait up to 30 seconds for completion. # Cart Purchase Status Source: https://docs.henrylabs.ai/api-reference/cart/cart-purchase-status /v1/api-reference/openapi.documented.json get /cart/purchase/status Check the status of a cart purchase job and get results when ready. # Cart Remove item Source: https://docs.henrylabs.ai/api-reference/cart/cart-remove-item /v1/api-reference/openapi.documented.json delete /cart/{cartId}/item Remove an item from a cart by its unique identifier # Cart Update Item Source: https://docs.henrylabs.ai/api-reference/cart/cart-update-item /v1/api-reference/openapi.documented.json put /cart/{cartId}/item Update an item in an existing cart. # Merchant Retrieve Source: https://docs.henrylabs.ai/api-reference/merchants/merchant-retrieve /v1/api-reference/openapi.documented.json get /merchants/{host} Fetch a single merchant by host with its per-method coverage statuses (checkout, productDetails, productSearch). The top-level coverageStatus field is a deprecated alias of coverage.checkout. Also returns searchFilters — the variant-option filter axes (name → display values) accepted by merchant-scoped Product Search for this merchant; null until computed. # Merchant Search Source: https://docs.henrylabs.ai/api-reference/merchants/merchant-search /v1/api-reference/openapi.documented.json get /merchants/search Search merchants by free text. Matches the query against merchant name and host (case- and accent-insensitive partial match), ranking prefix matches first. Returns merchant identity plus per-method coverage statuses, so results can be used directly to scope a Product Search (filters.merchant) or gate checkout affordances. # Merchants List Source: https://docs.henrylabs.ai/api-reference/merchants/merchants-list /v1/api-reference/openapi.documented.json get /merchants Fetch a paginated list of merchants with their per-method coverage statuses (checkout, productDetails, productSearch). The top-level coverageStatus field is a deprecated alias of coverage.checkout. # Orders List Source: https://docs.henrylabs.ai/api-reference/orders/orders-list /v1/api-reference/openapi.documented.json get /orders Fetch a list of orders with optional filtering and pagination. # Product Details Source: https://docs.henrylabs.ai/api-reference/product/product-details /v1/api-reference/openapi.documented.json post /product/details Fetch detailed information about a product from a given URL. Requests are async by default, or use mode=sync to wait up to 30 seconds for completion. # Product Details Status Source: https://docs.henrylabs.ai/api-reference/product/product-details-status /v1/api-reference/openapi.documented.json get /product/details/status Check the status of a product details retrieval job and get results when ready. # Product Search Source: https://docs.henrylabs.ai/api-reference/product/product-search /v1/api-reference/openapi.documented.json post /product/search Search for products by text query or image input. Requests are async by default, or use mode=sync to wait up to 30 seconds for completion. # Product Search Status Source: https://docs.henrylabs.ai/api-reference/product/product-search-status /v1/api-reference/openapi.documented.json get /product/search/status Check the status of a product search retrieval job and get results when ready. # Engineer Intern Source: https://docs.henrylabs.ai/v1/info/careers/engineer-intern Intern with Henry Labs in NYC and help build the future of agentic commerce * 📍 **Location: New York City (in-person)** * 💼 **Years of experience: 0+ (current students and recent grads welcome)** * 🛠️ **Skills: TypeScript, Bun, Postgres, Playwright, SDKs, API design, React** ## About Henry Labs We're building the **operating system for agentic commerce**: the layer that turns a user's intent to buy into a completed order on any merchant site. We enable a **true universal cart** and **one-click checkout anywhere on the Internet** - so users can add items from multiple merchants in one place and buy seamlessly, without bouncing through different tabs, logins, and carts. Platforms like Pinterest, Instagram, TikTok, and AI chatbots/shopping experiences already handle product discovery. What's missing is reliably executing checkout across thousands of ecommerce sites without direct integrations. That's where we come in. Our agent **executes checkout on behalf of users**, then returns order confirmation back to the app. Imagine being able to **buy an entire outfit you see on Pinterest in one click** - sizes, colors, shipping, and payment handled instantly - without bouncing through 6 different sites and carts. That's just one example of what we enable. Companies use us to embed shopping natively so that users can **buy what they want, the moment they discover it**. If you're excited about building agents and infrastructure that **revolutionizes how the world buys online**, we'd love to talk! ## About the role As an **Engineer Intern** at Henry Labs, you'll work directly with our founding team on the core systems that power our AI agent platform. You'll ship real code to production, own meaningful projects, and have a direct impact on how the product evolves - while growing fast as an engineer alongside a high-velocity team that's building something legitimately new. ## What you'll do * Build and ship features across our **product, developer tools, and APIs** * Work on **browser automation, agent infrastructure, and checkout reliability** * Pair directly with founders and senior engineers on hard problems * Contribute to a fast-moving codebase where your work reaches customers in days, not quarters ## What we're looking for More than any specific background, we care about how you think and operate. If the traits below sound like you, you'll fit in well here: * **Willing to learn, low ego:** you take feedback without getting defensive, hold your views lightly, and can point to times you reversed course or were plain wrong * **Intellectually curious:** you light up at novel problems, dig into unglamorous ones, and have real interests and depth outside of pure work * **Owns the outcome:** you proactively pick up problems, drive them end to end to a high standard without needing to be managed, and when something breaks - even if it isn't your fault - you take responsibility and act rather than externalizing it * **Competitive and driven:** you set a high personal bar and are energized - not drained - by the pace and intensity of an early-stage startup On the technical side: * Experience with - or willingness to learn - **TypeScript, React, browser automation, and modern web stacks** * Bonus: interest or experience in **fintech, payments, e-commerce, AI agents, or developer tools** ## What you can expect at Henry Labs * Direct mentorship from founders and senior engineers * Real ownership over meaningful projects * A path to a full-time offer for standout interns * Relocation assistance for the duration of the internship ## How to apply Add Lawrence on [LinkedIn](https://www.linkedin.com/in/lawrence-rx-lim/) and include: 1. Your resume 2. Past projects, work, and GitHub Excited to hear from you! # Founding Engineer Source: https://docs.henrylabs.ai/v1/info/careers/founding-engineer Join Henry Labs in NYC as a founding engineer * 📍 **Location: New York City (in-person)** * 💰 **Salary: \$120,000 - \$220,000 + 0.25% - 2% equity** * 💼 **Years of experience: 1+** * 🛠️ **Skills: TypeScript, Bun, Postgres, Playwright, SDKs, API design, React** ## About Henry Labs We're building the **operating system for agentic commerce**: the layer that turns a user's intent to buy into a completed order on any merchant site. We enable a **true universal cart** and **one-click checkout anywhere on the Internet** - so users can add items from multiple merchants in one place and buy seamlessly, without bouncing through different tabs, logins, and carts. Platforms like Pinterest, Instagram, TikTok, and AI chatbots/shopping experiences already handle product discovery. What's missing is reliably executing checkout across thousands of ecommerce sites without direct integrations. That's where we come in. Our agent **executes checkout on behalf of users**, then returns order confirmation back to the app. Imagine being able to **buy an entire outfit you see on Pinterest in one click** - sizes, colors, shipping, and payment handled instantly - without bouncing through 6 different sites and carts. That's just one example of what we enable. Companies use us to embed shopping natively so that users can **buy what they want, the moment they discover it**. If you're excited about building agents and infrastructure that **revolutionizes how the world buys online**, we'd love to talk! ## About the role As **Founding Engineer** at Henry Labs, you'll architect and build the core systems that power our AI agent platform end-to-end. You'll own critical infrastructure from conception to production, working with bleeding edge tech that needs to be both reliable and low latency. You'll also play a foundational role in how we build Henry Labs's culture. ## What you'll do * Design, develop, and scale new **product infrastructure, developer tools, and APIs** * Set direction for **critical technical surface areas**, enabling us to stay ahead of rapid growth * Build reliable, scalable backend systems that handle payment and checkout complexity * Shape Henry Labs's hiring decisions and **culture** as we grow ## What we're looking for * **1+ years of experience** building production software * Experience with **TypeScript, React, Supabase/Postgres, and API/SDK design** * Interest or experience in **browser automation, Playwright, or similar tooling** * Interest or experience in **fintech, payments, e-commerce, or developer tools** * **Technical excellence:** as a developer tool, our technical decisions are the product, and the details matter a lot. * **Low ego, high impact:** you care more about building something meaningful than who gets credit * **Start-up ready:** early stage sometimes means figuring it out as we go, and that energizes rather than drains you * **Excited to crush it:** we're moving fast and looking for people excited about high velocity, high impact work, and having fun along the way. ## What you can expect at Henry Labs * **Industry-competitive salary** * **Above market equity** in an early-stage company * Small, high-trust team of engineers * **Unlimited PTO** * Relocation assistance ## How to apply Add Lawrence on [LinkedIn](https://www.linkedin.com/in/lawrence-rx-lim/) and include: 1. Your resume 2. Past projects, work, and GitHub Excited to hear from you! # Join Us! Source: https://docs.henrylabs.ai/v1/info/careers/join-us Build the future of agentic commerce at Henry Labs ## About Henry Labs We're building the **operating system for agentic commerce**: the layer that turns a user's intent to buy into a completed order on any merchant site. We enable a **true universal cart** and **one-click checkout anywhere on the Internet** - so users can add items from multiple merchants in one place and buy seamlessly, without bouncing through different tabs, logins, and carts. Platforms like Pinterest, Instagram, TikTok, and AI chatbots/shopping experiences already handle product discovery. What's missing is reliably executing checkout across thousands of ecommerce sites without direct integrations. That's where we come in. Our agent **executes checkout on behalf of users**, then returns order confirmation back to the app. Imagine being able to **buy an entire outfit you see on Pinterest in one click** - sizes, colors, shipping, and payment handled instantly - without bouncing through 6 different sites and carts. That's just one example of what we enable. Companies use us to embed shopping natively so that users can **buy what they want, the moment they discover it**. If you're excited about building agents and infrastructure that **revolutionizes how the world buys online**, we'd love to talk! ## Open roles * [Founding Engineer](/v1/info/careers/founding-engineer) * [Engineer Intern](/v1/info/careers/engineer-intern) *** ## Contact us For more contact information, please read job descriptions. # Changelog Source: https://docs.henrylabs.ai/v1/info/changelog Product updates, fixes, and feature announcements ### Merchant List Filter for Global Search * Global text searches accept `filters.merchants`, a list of merchant hosts or URLs to restrict results to. See the [Product Discovery guide](/v1/sdk/server/guides/product-discovery). * Available in [`@henrylabs/sdk@1.18.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Statement Name * Charges Henry Labs makes on a buyer's card now read `HENRY LABS* ` on their statement, for example `HENRY LABS* ZARA`, instead of `HENRY* `. Merchant names longer than 10 characters are shortened. Your integration doesn't change. If you show buyers sample statement text, update it. ### Card Issuance (Beta) Card Issuance is in **beta**. Email [support@henrylabs.ai](mailto:support@henrylabs.ai) to request access. * Check out on a single-purpose virtual card paid for by your buyer's card. Unspent amounts are refunded automatically. See the [Issued Cards guide](/v1/sdk/server/guides/issued-cards). * `cart.checkout.purchase` accepts `checkoutDetailsRefIds` to size the card from a checkout quote. * Added `card.issue`, `card.retrieve`, `card.close`, and `card.reveal`. ### Order Status * Order `status` now reports the outcome: `complete`, `partially_complete` (see `products[].status`), or `failed` (see `error`). Previously a finished order always read `complete`. * If you poll until `complete`, also stop on `partially_complete` and `failed`. * Added the `order.purchase.partially_complete` webhook event. Each order fires exactly one of `order.purchase.complete`, `order.purchase.partially_complete`, or `order.purchase.failed` once its outcome is final. * `orders.list` accepts `partially_complete` and `failed` as `status` filters. ### Text Search * Global text search now returns products from Henry's catalog by default. * Searches that name a merchant, like "nike running shoes", return that merchant's products. * Results are priced in the currency of the market you search. ### Image Search * Global image search now includes products from Henry's catalog. The `x-henry-beta: image-index-search` header is no longer needed. * With `supportedOnly: true`, every image result can be bought through Henry. * Merchant search accepts `filters.imageUrl` for merchants whose catalog supports image search. ### Shipping Errors in Checkout Details * Checkout details now returns `SHIPPING_UNAVAILABLE` when a product can't ship to the buyer's address, listing each affected product with the merchant's reason. Currently for eBay listings. ### Hosted MCP Server * The hosted MCP server at `https://mcp.henrylabs.ai/mcp` now accepts your Henry API key as `x-api-key` or `Authorization: Bearer`, so agents and scripts can connect without signing in. * The local `@henrylabs/mcp` package is retired. See [MCP Server](/v1/sdk/server/mcp/remote-mcp) for setup. * The Henry plugin for Claude Code, Codex, and Cursor now uses the hosted server. ### SDK * Added `card.tokenize`, `card.updateCvv`, `merchants.retrieve`, and `merchants.search` to the SDK. * **Breaking:** removed `mintedCards` from `orders.list` and `cart.checkout.pollPurchase` responses. * Available in [`@henrylabs/sdk@1.17.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Headless Checkout for Every Team * Headless checkout (`cart.checkout.purchase`) is now enabled for every team, no request needed. Pricing is unchanged. See the [Headless Checkout guide](/v1/sdk/server/guides/checkout#headless-checkout). ### Price Filter * `filters.price` now applies to every merchant and to cached results. * Items priced in a different currency than `filters.price.currency` are returned rather than dropped. ### Price Filter * Product search now enforces `filters.price.min` and `filters.price.max` on returned prices. * When a product has several sellers, results use the first offer within your price range. * Pages may contain fewer than `limit` results - keep paging until `nextCursor` is `null`. ### Variant Prices * Product details `options` now include per-variant `price` and `originalPrice`. * Added the `discontinued` availability value for products or variants a merchant no longer sells. Search excludes them by default. ### Estimated Tax * Checkout details now returns `costs.tax`, an estimated sales tax for the shipping address, included in `costs.total`. ### Lower Credit Costs * Product details now costs **1 credit** (down from 2). * Checkout details now costs **1 credit** (down from 5). * See [Pricing](/v1/info/pricing). ### Sold-Out Products * Product search no longer returns out-of-stock products. * Pass `filters.includeOutOfStock: true` on a merchant search to include them. * Available in [`@henrylabs/sdk@1.14.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Buyer Emails * Turn each order email (received, complete, partially fulfilled, failed) on or off per app in Settings → App → General → Notifications. * Added an order-complete email, on by default. ### Webhooks for Guest Orders * Fixed: webhooks now fire for hosted checkout orders from buyers without a Henry account. ### Affiliate Rates * Merchant list and retrieve now return `affiliate.maxCommissionRate`, the highest commission rate Henry earns on that merchant. See [Merchants](/v1/sdk/server/guides/merchant). ### Affiliate Links * Product details now return an `affiliateLink` when Henry has affiliate coverage for the merchant. Purchases through it earn commission for your app. * Product details now rejects affiliate and tracking URLs in `link`. ### Merchant Search Filters * `merchants.retrieve` now returns `searchFilters`, the option values (like size and color) you can pass to merchant search. See [Merchants](/v1/sdk/server/guides/merchant). ### Merchant Search * Added `GET /v1/merchants/search` for searching merchants by name or host. See [Merchants](/v1/sdk/server/guides/merchant#search-merchants). * Fixed: the `name` filter on merchant list now matches partial names. ### Option Filters * Merchant search now accepts an `options` filter, like `{ "size": "M", "color": "black" }`. See [Product Discovery](/v1/sdk/server/guides/product-discovery). ### Merchant Coverage * Merchant list and retrieve now return `coverage` per method: `checkout`, `checkoutDetails`, `productDetails`, and `productSearch`. See [Merchants](/v1/sdk/server/guides/merchant). * Added matching coverage filters to merchant list. * `coverageStatus` is deprecated in favor of `coverage.checkout`. It still works. ### Expired Security Codes * `cart.checkout.purchase` now returns a `400` when the card's security code has expired. Re-collect it with the [CVC Element](/v1/sdk/client/elements/cvc-element) and retry. * An unknown `cardToken` now returns a `404`. ### Product Search * Global search now ranks products from checkout-supported merchants first. * Added a `supportedOnly` filter to global search. See [Product Discovery](/v1/sdk/server/guides/product-discovery). ### Buyer Emails * Set a per-app support email in Settings → App, used as the reply-to on buyer emails. * Choose whether failed-order emails include recovery links in Settings → App → Notifications. ### Service Fee Fields * **Breaking:** `commissionFeeFixed` and `commissionFeePercent` are now `serviceFeeFixed` and `serviceFeePercent` in `cart.create` settings. * **Breaking:** `costs.commissionFee` is now `costs.serviceFee` in order results and webhooks. * Update your integration to the new names. Carts using the old names fall back to your app's default fee. * Available in [`@henrylabs/sdk@1.10.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Cart * The default cart service fee is now **0%** (previously 5%). New carts are created with no service fee unless you explicitly set one. ### Checkout * Checkout details now return a single, unified `data` result shape across methods - easier to parse and consistent regardless of the underlying merchant. * Added shipping cost estimation, so you can surface expected shipping before a purchase completes. * Faster checkout page load and clearer inline form validation messages. ### Orders * Completed order results may now return `null` for `tax` and `shipping` when a merchant doesn't itemize them, instead of failing validation. ### Merchant Coverage * Expanded supported merchants for product search and details, including UNIQLO, Gap, Banana Republic, Mango, Nike, DSW, and Fashion Nova. ### Affiliate Product Links * Cart items now accept an optional `affiliateProductLink` field on `cart.create`, `cart.item.add`, and `cart.item.update`. Pass your partner tracking URL and Henry will use it for fulfillment while keeping the original `link` as the canonical product reference. * `affiliateProductLink` is also returned on purchase responses (`cart.checkout.pollPurchase`) and order list items, so you can reconcile attribution end-to-end. * If `affiliateProductLink` is omitted, Henry falls back to `link` - existing integrations are unaffected. * Available in [`@henrylabs/sdk@1.5.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Cart Fetch * Added `cart.fetch(cartId, { buyer? })` to retrieve a single cart by ID and optionally prefill buyer details in the returned `checkoutUrl` for hosted checkout. * Available in [`@henrylabs/sdk@1.4.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### Product Discovery * `products.search` now supports merchant-scoped search via `type: 'merchant'`, alongside existing text and image search. * `mode: 'sync'` added across `products.search`, `products.details`, and `cart.checkout.purchase` - wait up to 30s for results inline instead of polling. ### Cart * `cart.create` settings now accept the full `settings.events` trigger model, including webhook, email, points, and tier actions with optional conditionals. * Released in [`@henrylabs/sdk@1.3.0`](https://www.npmjs.com/package/@henrylabs/sdk). ### SDK * Published [`@henrylabs/sdk@1.2.1`](https://www.npmjs.com/package/@henrylabs/sdk) with method signature updates and polling helper refinements. ### SDK * Published [`@henrylabs/sdk@1.1.2`](https://www.npmjs.com/package/@henrylabs/sdk) with refreshed generated types. ### Henry SDK v1 * Shipped [`@henrylabs/sdk@1.0.0`](https://www.npmjs.com/package/@henrylabs/sdk) - the stable TypeScript server SDK for product discovery, universal cart, checkout, and orders. * Cart creation now returns a ready-to-use `checkoutUrl` alongside `cartId` - no separate session step needed. * Unified async pattern (`refId` + `poll*`) across product search, product details, and headless checkout, with a new `mode: 'sync'` option to wait up to 30s inline. ### Client SDK (Browser) * Introduced [`@henrylabs/js@1.0.0`](https://www.npmjs.com/package/@henrylabs/js) - a framework-agnostic browser SDK for embedding checkout directly into your frontend. * `Henry.createCheckoutElement(...)` drops a buy-now button and modal onto any page with display modes for full, compact, comfortable, bubble, and direct. * `Henry.createCardElement(...)` renders an isolated iframe for PCI-safe card capture and returns a `cardToken` for server-side use with headless checkout. ### MCP Improvements * Added initial server instructions for MCP. ### Bug Fixes * Fixed a memory leak related to abort signals in the client. * Fixed an issue where abort listeners were removed too early. * MCP no longer falls back to baseUrl when the environment variable is set. * Correctly defined the `false` enum type. ### Improvements * Merged staging into main. ### Bug Fixes * Fixed MCP installation instructions for remote servers. * MCP now allows fallback behavior for required environment variables. ### Transaction Fee on Checkout * Charge users a transaction fee on checkout. This fee is applied to the total order amount. Currently for Enterprise customers only. ### Saved Shipping Addresses for Guest Users * Now guest users can save multiple shipping addresses ### Checkout MCP * Bug fixes and performance improvements ### Concurrent Enrich & Variant Check Requests * Enrich and check variants in parallel, with up to 50 concurrent requests. * Now possible to batch enrich and check variants ### Improved Product Search Filtering * Updated product text and image search parameters to include additional filters and sorting options. * Filter by brand, merchant, min/max price, sale status, and more. * Scope searches to country and region (Ex. us, Austin, Texas) * Sort results by relevance and price ### Multi-Cart Checkouts * Checkout multiple items at once with our multi-cart checkout flow. Multi-cart checkout will work automatically without any code changes. * Users can add items to cart from any supported merchant, and they will be checked out together. Adding multiple items in a single request is currently experimental. ### Save Additional Addresses & Payment Methods * Save additional address and payment methods for your users, making it easier than ever to place orders. * This feature is released along side with a visual refresh of our hosted checkout page. ### Enrich Top-k Products * Now automatically enrich top-k products returned from Text Product and Image Product searches. ### Custom Themes * In addition to adding your brand assets to emails and our hosted checkout page, you can now customize the color scheme of our checkout page and purchase confirmation email. ### Order Management * Query all orders for a given user using our new Order List endpoint ### Product Discovery * Introducing reverse image search. Search through 2b+ SKUs, enabling more intuitive and visual shopping experiences in your app. ### Hosted Checkout * Added iFrame support with cookie-free checkout. Embed the checkout experience directly in your app without worrying about third-party cookie restrictions or cross-origin limitations. ### Programmatic Email Inbox * All transactional emails, order confirmations, shipping updates, newsletters, and more are now captured and stored. * Query and access your users' merchant emails programmatically, giving you full control to build custom order tracking, notifications, and support experiences. ### Global Region Support * Now, allow users from anywhere in the world to route checkouts through Henry Labs * Initial support is for EU and UK regions in addition to existing US support. Wider global support coming soon ### Product Enrichment Endpoints * Introduced Product Enrichment Endpoints, allowing you to get product price, variant information, stock availability for a product given its link * Endpoint is currently Experimental, with availability reserved for Plus Plan customers and above ### API Playground * Added new API Playground for Sandbox and Production environments. Get started [here](https://docs.henrylabs.ai/info/playground) * Intended for developers trying out our APIs. It's recommended to save the Directory link to keep your documentation up to date with our latest API changes ### Basic, Standard, and Plus Plans * Rolled out self-serve subscription plans for customers of all sizes. * No matter what stage your business is in, Henry Labs has a plan for you. Build embedded shopping experiences in your app today. ### AI Agent in Quickstart Demo * Added AI Agent to Quickstart Demo. Try it out [here](https://demo.henrylabs.ai/ai). * This agent is powered by [Henry Shopping MCP](https://www.npmjs.com/package/@henrylabs/mcp) ### Enterprise Pro Plan * Introduced Enterprise Pro Plan, with full access to headless checkout, bespoke API features, 24/7 support, and more. For more information contact [support@henrylabs.ai](mailto:support@henrylabs.ai). ### Email Whitelabeling * Email domain whitelabeling has been rolled out in production. Route merchant emails through your own domain, allowing you to control the end-to-end customer experience without comprimising user trust or ever managing returns. ### Checkout & Payment Experience * Visual refresh. Updated payment card designs, icons, dropdowns, and more. * Improved UI for CVV updates, payment method management, and shipping address autocomplete. ### Product Search & Quickstart Demo * Expanded product details API to include reviews, video, stars, similar products, and related searches. * Quickstart Demo now renders new product details on `demo.henrylabs.ai`. ### Hosted Checkout Whitelabeling * Add your brand logo to the hosted checkout modal. ### Henry Managed Accounts * Introducing Henry Managed Accounts. Unlock the power of user merchant accounts fully managed by Henry Labs. * Track post-order states (shipping, returns, etc) so you can provide your customers real-time order updates. * Whitelabel merchant emails, build trust with your customers, and have full control over the E2E user experience. * Managed accounts are rolling out to all supported merchants ### Hosted Checkout * Hosted checkout now supports updating CVV at point of checkout ### Product Discovery * The result of variant checks is now reflected in our Hosted Checkout page ### Henry Shopping SDK (TypeScript) * Introducing the Henry Shopping SDK, making it easier than ever to start building (Python SDK coming soon) ### Henry MCP Server * Add an end-to-end embedded commerce stack to your agent application with less than 10 lines of code ### Universal Cart * `POST /cart/items` now accepts `affiliateProductLink`, letting you pass through your partner tracking links end-to-end. * Added the optional `checkVariantAvailability` parameter so carts can auto-check the selected variant's inventory when items are added. Feature is currently experimental. * Introduced a "blocklist" mode that allows any product to be added to cart and checked out except for those explicitly blocked. ### Wallet * Added guest card collection mode for `/wallet/cart-collect` ### Product Discovery * Released experiemental variant check endpoints. Use them to check real time stock availability of variants. ### Henry Dashboard * Added staging vs. production environment flag to dashboard. ### Miscellaneous * Added unified generated documentation at `api.[sandbox].henrylabs.ai/docs` # Contact Us Source: https://docs.henrylabs.ai/v1/info/contact-us Connect with the Henry Labs team We are here to help you build with Henry. Reach out through email or book a meeting that fits your schedule. ## Email Support [Email us](mailto:support@henrylabs.ai) with questions about onboarding, integrations, or anything else you need to start building. ## Schedule Time Prefer to talk live? [Book a 30-minute call](https://calendly.com/lawrence-henry-social/30min) and we'll walk through your flow together. # Pricing Source: https://docs.henrylabs.ai/v1/info/pricing Plans and per-request credit costs for the Henry Shopping API Choose the plan that fits — start for free, then scale as you grow. ## Plans **\$0** / forever A lightweight way to get started. No card, no hassle. * 100 credits total * Up to 20,000 SKUs * 1 seat * Product Data * Embedded Checkout * Analytics Dashboard * 50,000+ affiliate links * Transaction Fee capability * Overage fees apply **\$30** / month Great for side projects and small tools. Cancel anytime. * 3,000 credits / month * Up to 120,000 SKUs * 1 seat * Product Data * Embedded Checkout * Analytics Dashboard * 50,000+ affiliate links * Transaction Fee capability * Overage fees apply **\$250** / month For teams shipping production traffic. Cancel anytime. * 30,000 credits / month * Up to 1,200,000 SKUs * Unlimited seats * Product Data * Embedded Checkout * Analytics Dashboard * 50,000+ affiliate links * Transaction Fee capability * Overage fees apply **Let's talk** — custom pricing For platforms operating at scale. * Unlimited credits * Unlimited SKUs * Unlimited seats * Product Data * Embedded Checkout * Analytics Dashboard * 50,000+ affiliate links * Transaction Fee capability * [Contact us →](mailto:support@henrylabs.ai) ## Credits per request Pay for what you actually use. Every paid plan includes a monthly credit allowance; once it's spent, overage rates apply. | Request | Credits | | :- | :- | | Product Search | 1 | | Product Details | 1 | | Checkout Details | 1 | | Checkout Purchase | 50 | ## Questions? Need a custom plan, volume pricing, or help estimating credit usage? [Reach out to our team](mailto:support@henrylabs.ai) and we'll work through it with you. # Privacy Policy Source: https://docs.henrylabs.ai/v1/info/privacy-policy How Henry Labs collects, uses, shares, and protects information across its websites, APIs, SDKs, MCP server, and hosted checkout. *Last updated: 09/02/2026* **Henry Social, Inc.** ("**Henry**," "**we**," or "**us**") is committed to protecting your privacy. This Privacy Policy describes how we collect, use, share, and protect information in connection with: * Our websites, dashboards, hosted checkout pages, widgets, and browser-based experiences; * Our APIs, SDKs, MCP server, and other developer tools; and * Any other online services that link to this Privacy Policy (collectively, the "**Sites**" or "**Services**"). This Privacy Policy applies to: * Developers and other businesses that integrate Henry ("**Customers**" or "**Developers**"); * End users who interact with Henry-powered checkouts or other flows ("**End Users**"); and * Visitors to our marketing sites and docs. By using the Services, you agree to the practices described in this Privacy Policy. If you do not agree, you should not use the Services. *** ## **1. Key Definitions** **"Personal Data"** means information that identifies, relates to, describes, or can reasonably be linked to an identified or identifiable individual. This can include your name, email address, shipping address, IP address, and similar data. **"Sites"** means our websites, applications, widgets, blogs, social network pages, or other online, mobile, or wireless offerings and the Services we provide through them. **"Services"** means our infrastructure and tools that enable applications, agents, and other interfaces to facilitate shopping and checkout experiences across online merchants, including our APIs, SDKs, hosted checkout pages, and dashboards. **"Merchants"** means third-party brands, retailers, and other merchants whose products or services you may purchase using Henry-powered flows. **"Merchant of Record"** means the party that sells a product or service to you, charges your payment method, and is responsible for that sale. Depending on the Merchant, the product, your location, and how the transaction is fulfilled, the Merchant of Record may be the Merchant, Henry or a Henry affiliate, or another party designated by Henry or the Merchant. The Merchant of Record may differ between transactions. Our Terms of Service explain what this means for your order. **"Customer Data"** means content, data, or information that a Customer submits to the Services or that is generated through a Customer's integration, including Personal Data about End Users, as described in Section 2.2. **"Connected Email Account"** means an email account (such as Gmail) that you optionally connect to the Services through an authorization flow (such as OAuth) to enable post-purchase features (for example, order tracking updates). **"Order Tracking Data"** means limited order-related fields we may derive from order confirmations and shipping update messages (for example, merchant name, order identifier, shipment status, carrier, tracking number, timestamps, delivery estimate, and related order metadata). *** ## **Gmail Connection Summary (Plain-English)** **Connecting Gmail (optional).** If you choose to connect your Gmail account to Henry using Google's sign-in (OAuth), we use that access to help you find and track your orders. **What we look for.** We may process shopping-related emails in your connected Gmail (for example: order confirmations, shipping notices, delivery updates, return/refund messages) so we can create and update your order history and tracking status. **What we store.** We aim to extract and store order-tracking details (like merchant name, order number, tracking number, carrier, shipment/delivery timestamps, and status) rather than keeping full email content. **No ads from Gmail data.** We do not use information from your connected Gmail account to advertise to you, build an advertising profile, or sell that information. **Who can access it.** Our systems process this information automatically. Humans at Henry don't review your Gmail content unless (1) you ask us for support, (2) it's needed to investigate abuse/security, or (3) we're required by law. **You control it.** You can disconnect Gmail at any time. After you disconnect, we stop accessing your Gmail account, and you can request deletion of any Gmail-derived order data as described below. **Google's rules apply.** Henry's use and transfer to any other app of information received from Google APIs will adhere to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes), including the Limited Use requirements. See Section 3.7. *** ## **2. Information We Collect** The Personal Data we collect falls into several broad categories. ### **2.1 Information You Provide Directly** We may collect Personal Data that you choose to provide when you: * Sign up for a Henry account or access the Services as a Customer or Developer; * Use Henry-powered checkout or other flows as an End User; * Integrate our APIs or SDKs into your product; * Communicate with us (for example, by email or through support channels); or * Subscribe to our newsletters or marketing communications. This may include: **Identifiers:** Name, email address, phone number, company, job title, username, and password. **Commercial Information:** Billing address, shipping address, transaction and order information related to your use of Henry or Henry-powered checkouts, including items purchased, cart contents, and order history. **Account & Integration Information:** API keys, integration settings, merchant account identifiers, and configuration details you use with our SDKs or APIs. **Payment Information:** We rely on third-party payment processors to handle payments. We do not store full credit or debit card numbers on our own systems. Card details are stored by a third-party payment vault provider that maintains PCI DSS compliance, and Henry is able to access the full card information held by that provider, including the card number, in order to complete and support the transactions you authorize. We also receive limited information related to your payment (such as the last four digits of your card, card type, and transaction status) from our payment processors and/or Merchants. Where Henry or a Henry affiliate is the Merchant of Record for a transaction, we receive that transaction and billing information directly from our payment processors, and we keep the billing, tax, and order records we are required to maintain as the seller for that transaction. **Support and Communications:** Content of messages you send us, including support tickets, feedback, or other communications. **Connected Email Account Information (Optional):** If you choose to connect an email account (such as Gmail) to enable post-order tracking features, we may receive and store authorization tokens and related account metadata (such as the email address associated with the connection, and timestamps for when the connection was established or revoked). We may also access and process messages from the Connected Email Account as described in Section 2.5 and Section 3.7. ### **2.2 Information We Receive from Customers / Developers** Customers and Developers may submit Personal Data to us through the Services, for example when they: * Integrate Henry with their own applications, agents, or services; * Connect a Merchant account or other platform to Henry; or * Use Henry to facilitate checkout for their users. Depending on the integration, this may include: **Identifiers:** Names, email addresses, phone numbers, shipping addresses, and other contact details for End Users or other individuals. **Commercial Information:** Product details, cart contents, order history, and other purchase-related information. **Interaction Data:** Information about how End Users interact with Henry-powered flows, such as steps completed in a checkout or error states. Customers are responsible for their own privacy and security practices. Where we process Personal Data on behalf of a Customer, the Customer's privacy policy may also apply, and the Customer generally controls how that data is used. ### **2.3 Information We Automatically Collect** Like many online services, we automatically collect certain information when you visit our Sites or use our Services. We may use cookies, pixels, and similar technologies to collect some of this information. See our Cookie Policy for more details. Categories of automatically collected data may include: **Device Information:** IP address, operating system, browser type and version, device identifiers, language settings, and other technical details about the device and applications you use to access the Services. **Log Data:** Logs about how you access and use the Services, including timestamps, URLs, referrer URLs, response times, error codes, user agent strings, and other diagnostic information. We may also access metadata associated with files you upload or transmit through the Services. **Product Usage Data:** Information about how you interact with the Services, such as which features you use, which endpoints you call, your interactions with hosted checkout pages, and performance metrics related to our Services. We may aggregate or de-identify this information so that it can no longer reasonably be linked to a particular individual. ### **2.4 Information We Collect from Third Parties** We may obtain information about you from third-party sources, including: * Public databases and public profiles on social media platforms; * Service providers and partners (for example, analytics providers, fraud prevention services, and payment processors); * Merchants and other platforms with which we integrate. We take reasonable steps to ensure that such third parties are permitted to share this information with us. ### **2.5 Information We Process from Connected Email Accounts (Optional)** If you connect a Connected Email Account, we may access and process messages **only as needed to provide the specific user-facing features you enable** (for example, post-order tracking). Message content can include order confirmations, shipping notices, delivery updates, cancellations, returns, and refunds. We use this content to derive Order Tracking Data. We do **not** require or request your email password for Connected Email Accounts and instead rely on the permissions you grant through the applicable authorization flow (such as OAuth). You may revoke access at any time as described in Section 6. *** ## **3. How We Use Personal Data** We use the Personal Data we collect for a variety of purposes, including: ### **3.1 To Provide and Operate the Services** * Facilitating Henry-powered checkouts and other shopping flows; * Routing orders and order information between Customers, End Users, and Merchants; * Creating and managing your Henry account; * Operating APIs, SDKs, dashboards, and other tools; * Providing customer support and responding to your inquiries. ### **3.2 To Secure and Maintain the Services** * Monitoring, preventing, and detecting fraud, abuse, security incidents, and other harmful activity; * Managing and protecting the integrity of our infrastructure and systems; * Debugging and troubleshooting problems; * Enforcing our Terms of Service and other agreements, and protecting our rights and those of others. ### **3.3 To Analyze and Improve the Services** * Understanding how the Services are used; * Developing new features, products, and services; * Improving performance, reliability, and user experience; * Conducting research, analytics, and A/B testing. ### **3.4 To Communicate with You** * Sending you service-related messages (such as account notices, technical updates, or security alerts); * Responding to your questions and support requests; * Sending you marketing communications about Henry (where permitted by law), including product updates, events, and other information we think may interest you. You can opt out of marketing communications at any time. ### **3.5 To Support Advertising and Marketing** * Using cookies and similar technologies to measure the effectiveness of our marketing; * Showing you tailored advertising on our Sites and, where applicable, on other websites and platforms, through third-party advertising partners (subject to your choices and applicable law). ### **3.6 To Comply with Legal Obligations** * Complying with applicable laws, regulations, and legal processes; * Responding to lawful requests from public authorities; * Fulfilling our tax, accounting, and auditing obligations. ### **3.7 Connected Email Accounts and Order Tracking (Optional)** If you connect a Connected Email Account, we use data from that account **only to provide user-facing order features you enable**, such as: * Identifying order confirmations and extracting order identifiers; * Identifying shipping notices and extracting carrier and tracking numbers; * Detecting delivery status changes (for example, shipped, out for delivery, delivered, delayed/exception); * Detecting cancellations, returns, or refund notices; and * Providing order updates to you and, if you are using a Customer integration, to the Customer application so it can display order updates to you. **Data Minimization.** We use reasonable efforts to minimize what we access and store. In general, we aim to store **Order Tracking Data** and related identifiers (such as message IDs and timestamps) rather than storing full email bodies. **No Ads / No Sale / No Marketing Profiling for Email Data.** We do not use data obtained from Connected Email Accounts (including message content or Order Tracking Data) to serve advertisements (including personalized or retargeted ads), do not sell it, and do not use it for marketing profiling or for determining eligibility for credit, lending, or insurance. **Limited Use (Google).** Henry's use and transfer to any other app of information received from Google APIs will adhere to the [Google API Services User Data Policy](https://developers.google.com/terms/api-services-user-data-policy#additional_requirements_for_specific_api_scopes), including the Limited Use requirements. We may also use Personal Data for any other purpose described at the time we collect it or with your consent. *** ## **4. How We Share Personal Data** We may share Personal Data in the following ways: ### **4.1 Affiliates and Service Providers** We share Personal Data with our affiliates and with third-party service providers who help us operate, secure, and improve the Services. These service providers may assist us with: * Hosting and infrastructure; * Payment processing; * Analytics and performance monitoring; * Security and fraud prevention; * Customer support and communications; * Marketing and advertising. These service providers are authorized to use Personal Data only as necessary to provide services to us and are subject to appropriate confidentiality obligations. ### **4.2 Merchants** When you use Henry to facilitate a purchase from a Merchant, we share relevant Personal Data with that Merchant and/or its service providers as necessary to: * Process your order; * Arrange shipping or delivery; * Handle returns, exchanges, or customer service; * Comply with the Merchant's legal obligations. Merchants are independent third parties with their own privacy and data practices. Their use of your Personal Data is governed by their respective privacy policies and terms, not by this Privacy Policy. **Where Henry is the Merchant of Record.** For transactions in which Henry or a Henry affiliate is the Merchant of Record, we act as the controller (or the equivalent role under applicable law) of the Personal Data we process to complete that sale, and this Privacy Policy governs that processing. We still share the Personal Data necessary for the applicable Merchant or supplier to fulfill, ship, and support your order, and that party's own privacy policy governs its independent use of that data. ### **4.3 Customers / Developers** If you interact with Henry through a Customer or Developer (for example, through an AI shopping agent, application, or platform that integrates Henry), we may share Personal Data and usage information related to that integration back to the Customer, including: * Transaction and order information; * Logs and metrics relevant to the Customer's use of the Services; and * Contact or profile information where the Customer already has a relationship with you. **Order Tracking Data from Connected Email Accounts.** If you connect a Connected Email Account for post-order tracking and you are using a Customer integration, we may share **Order Tracking Data** with the Customer so the Customer can display order updates to you. We do not share full email bodies with Customers for this purpose. We expect Customers to use such information in accordance with their own privacy policies and applicable law. ### **4.4 Marketing and Advertising Partners** Through cookies and similar technologies, we may share limited information (such as cookie identifiers, IP address, and usage data) with third-party marketing and advertising partners (such as Google, Meta, and others) to help us: * Measure the effectiveness of our marketing; and * Provide tailored advertising on our Sites and, where applicable, on other websites or apps you may visit. **We do not share data obtained from Connected Email Accounts (including message content or Order Tracking Data) with marketing or advertising partners.** You can manage cookie preferences through your browser settings and, where available, through our cookie banner. See our Cookie Policy for details. ### **4.5 De-Identified and Aggregate Data** We may share de-identified or aggregate data with Customers, partners, or other third parties to help them understand trends and usage patterns. This data cannot reasonably be used to identify an individual and is not considered Personal Data. ### **4.6 Legal and Safety Reasons** We may disclose Personal Data if we believe in good faith that such disclosure is reasonably necessary to: * Comply with any applicable law, regulation, legal process, or governmental request; * Enforce our Terms of Service or other agreements; * Protect the rights, property, or safety of Henry, our users, Merchants, or the public; * Detect, prevent, or otherwise address fraud, security, or technical issues. ### **4.7 Business Transfers** In connection with any merger, acquisition, financing, reorganization, sale of assets, bankruptcy, or similar transaction, Personal Data may be transferred to a successor or affiliate as part of the transaction. If such a transfer occurs, we will require the transferee to honor this Privacy Policy or provide notice and, where required, obtain your consent. *** ## **5. Legal Bases for Processing (EEA / UK Individuals)** If you are located in the European Economic Area ("EEA") or the United Kingdom, we process your Personal Data on one or more of the following legal bases: **Performance of a Contract:** To provide the Services, process transactions, and fulfill our obligations under our agreements with you or the Customer you act for. **Legitimate Interests:** To operate, secure, and improve our Services; prevent fraud and misuse; conduct analytics and marketing; and support our business operations, provided that our interests are not overridden by your rights and interests. **Consent:** Where required by law (for example, certain marketing communications or non-essential cookies), we rely on your consent. You may withdraw consent at any time, without affecting the lawfulness of processing before withdrawal. **Legal Obligations:** To comply with applicable laws and regulations (for example, tax and accounting requirements, responding to law enforcement requests, and maintaining records of your privacy rights requests). *** ## **6. Your Privacy Choices and Rights** ### **6.1 Marketing Communications** You may opt out of receiving marketing emails from us at any time by: * Clicking the "unsubscribe" link in the email; or * Contacting us at **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)**. Even if you opt out of marketing communications, we may still send you service-related emails (for example, about your account, security alerts, or important updates to our terms and policies). ### **6.2 Rights Under Certain Laws (e.g., CCPA, GDPR)** Depending on your jurisdiction, you may have certain rights regarding your Personal Data, such as: * The right to know whether we process your Personal Data and to access it; * The right to correct inaccurate Personal Data; * The right to request deletion of your Personal Data (subject to certain exceptions); * The right to restrict or object to certain processing; * The right to data portability; * The right not to be discriminated against for exercising your privacy rights. If you are a California resident or an EEA/UK resident, additional, more specific rights may apply under the California Consumer Privacy Act ("CCPA"), the EU General Data Protection Regulation ("GDPR"), or the UK GDPR. To exercise these rights, please contact us at **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)** with sufficient detail for us to understand and respond to your request. We may need to verify your identity before fulfilling your request. In some cases, we may direct you to contact the relevant Customer (for example, where we process your data solely on a Customer's behalf). If you are in the EEA or UK, you also have the right to lodge a complaint with your local data protection authority. ### **6.3 Developer / Customer Communications** If you no longer wish to be contacted by a Customer or Developer that uses Henry, please unsubscribe directly from that Customer's communications or contact them to update or delete your information. Where appropriate, you can also contact us, and we may notify the Customer of your request, but we may not control how the Customer handles your data. ### **6.4 Connected Email Accounts (Disconnect / Revoke)** If you connected a Connected Email Account (such as Gmail) for post-order tracking, you may disconnect or revoke access at any time: * Through the Services (if we provide a self-service disconnect feature); and/or * By contacting us at **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)**. After disconnection, we will stop accessing the Connected Email Account. We will retain and delete Order Tracking Data in accordance with Section 11 (Data Retention) and applicable law. *** ## **7. International Transfers** Henry is based in the United States, and we and our service providers may process Personal Data in the United States and other countries that may have different data protection laws than your country. Where required by law, we implement appropriate safeguards (such as standard contractual clauses) to protect Personal Data transferred from the EEA/UK or other regions to the United States or other countries. You may contact us for more information about these safeguards. *** ## **8. Children's Privacy** Our Services are not directed to children under the age of 16, and we do not knowingly collect Personal Data from children under 16. If you become aware that a child has provided us Personal Data in violation of this Privacy Policy, please contact us at **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)**. If we learn that we have collected Personal Data from a child under 16, we will take reasonable steps to delete that information. *** ## **9. Do Not Track** Some browsers include a "Do Not Track" ("DNT") setting that signals to websites that you do not want your online activity tracked. The industry has not yet established a standard for responding to DNT signals, and we do not currently respond to DNT browser signals. You can manage your cookie preferences through your browser settings and, where available, through our cookie banner. *** ## **10. Cookies and Similar Technologies** We use cookies, pixels, and similar technologies to: * Help our Sites and Services function properly; * Remember your preferences; * Understand how you use our Services; and * Provide and measure the effectiveness of advertising. You can control cookies through your browser settings and, where available, through our cookie banner. For more information, please see our Cookie Policy. *** ## **11. Data Retention** We retain Personal Data for as long as necessary to: * Provide the Services and fulfill the purposes described in this Privacy Policy; * Comply with legal, tax, and accounting obligations; * Resolve disputes and enforce our agreements; and * Maintain business records consistent with our retention policies. **Connected Email Accounts and Order Tracking Data.** If you connect a Connected Email Account, we retain Order Tracking Data and related identifiers (such as message IDs and timestamps) for as long as necessary to provide the post-order tracking feature, support the Customer integration you are using, maintain records and logs for security and reliability, and comply with legal obligations. We aim to avoid storing full email bodies, and where we temporarily process email content, we use reasonable efforts to limit retention to what is necessary for debugging, security, and reliability. When we no longer have a legitimate need to process Personal Data, we will delete or de-identify it, or, if deletion is not possible (for example, because it is stored in backup archives), we will securely store it and isolate it from further processing until deletion is possible. *** ## **12. Security** We take reasonable technical and organizational measures designed to protect Personal Data from loss, misuse, unauthorized access, disclosure, alteration, and destruction. These measures may include encryption in transit, access controls, and regular security reviews. However, no method of transmission over the internet or method of electronic storage is completely secure. We cannot guarantee absolute security. **Access Controls for Connected Email Accounts.** We restrict access to Connected Email Account data and email-derived data (including Order Tracking Data) to authorized personnel and systems on a need-to-know basis. We do not permit humans to read email content obtained through Connected Email Accounts except as necessary to provide user support requested by you, for security purposes (such as investigating abuse), or to comply with law. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. *** ## **13. Changes to This Privacy Policy** We may update this Privacy Policy from time to time. When we do, we will revise the "Last updated" date at the top of this page and may provide additional notice (such as by email or in-product notice) where required by law. Your continued use of the Services after an updated Privacy Policy is posted constitutes your acceptance of the changes. If you do not agree with the updated Privacy Policy, you should stop using the Services. *** ## **14. Contact Information** If you have any questions, concerns, or complaints about this Privacy Policy or our data practices, please contact us at: **Henry Social, Inc.** Attn: Privacy Officer 265 Lorimer St 401 New York, NY, 11206 US Email: **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)** # Terms of Service Source: https://docs.henrylabs.ai/v1/info/terms-of-service The terms that govern your use of Henry Labs websites, dashboards, APIs, SDKs, MCP server, and hosted checkout. *Last updated: 09/02/2026* Please read these User Terms of Service (these "**Terms**" or this "**Agreement**") carefully. They form a binding agreement between you and **Henry Social, Inc., a Delaware corporation** ("**Henry**," "**Company**," "**we**," or "**us**"). These Terms, together with our Privacy Policy and any additional policies or disclosures referenced on our sites, govern your use of any Henry websites, dashboards, applications, hosted checkout pages, APIs, SDKs, browser extensions, social media pages, or other online services that link to these Terms (collectively, the "**Sites**" and our "**Services**"). Our Services help applications, agents, and other interfaces enable shopping and checkout experiences across online merchants ("**Merchants**"). The merchant of record for a purchase made through the Services may be the Merchant, Henry or a Henry affiliate, or another party we designate, depending on how that transaction is fulfilled. Section 2.3 and Section 9 explain what this means for your order. By accessing or using the Services, you agree to be bound by these Terms. If you do not agree, you may not use the Services. *** ## **1. Changes to These Terms and to the Services** We may update these Terms from time to time in our sole discretion. When we do, we will revise the "Last updated" date above and may provide additional notice (such as by email or in-product notice) where required by law. The updated Terms will be effective when posted, unless a later effective date is stated. If you continue to access or use the Services after updated Terms are posted, you are agreeing to the updated Terms. If you do not agree, you must stop using the Services. Because our Services evolve over time, we may change, add, or remove features, or suspend or discontinue all or part of the Services, at any time, without liability to you. **PLEASE NOTE: THESE TERMS CONTAIN AN ARBITRATION AGREEMENT AND A CLASS ACTION WAIVER. EXCEPT FOR CERTAIN TYPES OF DISPUTES, YOU AGREE THAT DISPUTES BETWEEN YOU AND HENRY WILL BE RESOLVED BY BINDING INDIVIDUAL ARBITRATION AND YOU WAIVE YOUR RIGHT TO A JURY TRIAL AND TO PARTICIPATE IN CLASS OR REPRESENTATIVE ACTIONS. SEE SECTION 20 BELOW.** *** ## **2. Roles and Relationships** ### **2.1 Henry** Henry provides infrastructure that allows applications, agents, and similar interfaces to: * Discover products or services offered by Merchants, and * Facilitate the placement of orders and payments with Merchants at your direction. Except where Henry or a Henry affiliate is the Merchant of Record for a transaction (see Section 2.3), Henry acts as a technical facilitator of your interactions with Merchants. In all cases, Henry does not manufacture the products or services you purchase, and does not control the Merchants or suppliers that produce or fulfill them. ### **2.2 Merchants** Each Merchant is a third party that offers products or services through the Services. Where the Merchant is the Merchant of Record for your purchase (see Section 2.3): * Your contract of sale is between you and the Merchant, not Henry; * The Merchant is responsible for pricing, product descriptions, inventory, shipping, returns, warranties, customer service, and compliance with applicable laws; and * The Merchant or its payment provider, and not Henry, charges your payment method. In that case, Henry does not guarantee the Merchant or any product or service it offers. We may or may not have a direct commercial relationship with a given Merchant in connection with your specific transaction and, even where such a relationship exists, we are not liable to you for that Merchant's acts, omissions, or failure to perform its obligations to you. ### **2.3 Merchant of Record** The "**Merchant of Record**" for a transaction is the party that sells the product or service to you, charges your payment method, and is responsible for that sale as described in Section 9. Depending on the Merchant, the product, your location, and how the transaction is fulfilled, the Merchant of Record may be: * The Merchant; * Henry or a Henry affiliate; or * A payment processor, reseller, importer of record, or other party designated by Henry or by the Merchant. The Merchant of Record may differ between transactions, between items in the same order, and over time. Nothing in these Terms is a commitment that any particular party will be the Merchant of Record for any particular transaction. If you want to know which party is the Merchant of Record for a particular order, you may contact us using the details in Section 24. ### **2.4 Customers and End Users** In some cases, you may access the Services because an organization (a "**Customer**")-for example, a developer, business, or platform-has integrated Henry into its own product or has invited you to use the Services. That Customer may have entered into a separate written agreement with Henry (a "**Customer Agreement**"). * If you are an End User accessing Henry through a Customer integration, these Terms govern your use of the Services, and the Customer Agreement governs Henry's relationship with the Customer. * As between Henry and the Customer, the Customer may own or control "Customer Data" as described in Section 10. If there is a conflict between these Terms and a Customer Agreement, the Customer Agreement will control with respect to Henry's obligations to the Customer. Henry does not act as your legal, financial, or fiduciary agent. Except where Henry or a Henry affiliate is the Merchant of Record and therefore sells to you as a principal, Henry acts as a technical intermediary, and any "on your behalf" actions by Henry are limited to carrying out your instructions to interact with Merchants and service providers. *** ## **3. Eligibility** By accessing or using the Services, you represent and warrant that: 1. You have read, understand, and agree to be bound by these Terms; 2. You are at least eighteen (18) years old (or the age of majority in your jurisdiction, if higher); and 3. You have the legal authority to enter into this Agreement on your own behalf, or on behalf of a company or other entity that has authorized you to use the Services. We do not knowingly allow children under 13 to use the Services. If we become aware that a child under 13 has created an account, we may delete the account and associated data. If you previously have been suspended or banned from using the Services, you are not permitted to access them. *** ## **4. Accounts, Credentials, and Security** ### **4.1 Account Registration** To use certain features (for example, dashboards, APIs, or hosted checkout management), you may be required to create an account ("**Account**") via: * A registration form where you provide certain information; and/or * A third-party login (such as OAuth, social login, or SSO), as described in our Privacy Policy. You agree to provide and maintain accurate, current, and complete information for your Account. If you provide information that is inaccurate or incomplete, we may suspend or terminate your Account. ### **4.2 License and Access** Subject to these Terms, Henry grants you a limited, revocable, non-exclusive, non-transferable license to access and use the Services for their intended purpose. We may suspend, limit, or revoke your access at any time if we believe, in our sole discretion, that you have violated these Terms, pose a risk to the Services or other users, or as otherwise permitted by law. We do not promise to continue operating any particular part of the Services or to maintain them in a particular form. ### **4.3 Credentials and Henry-Managed Access** You may provide Henry with credentials or tokens (for example, for email accounts, payment methods, or Merchant accounts), or Henry may create or manage accounts, email aliases, or identifiers on your behalf to facilitate transactions with Merchants ("**Access Credentials**"). By providing Access Credentials or authorizing Henry to manage them, you: * Represent that you are authorized to grant Henry access to use those credentials as necessary to provide the Services; * Authorize Henry and our service providers to use, store, and refresh those credentials, and to act using those credentials as reasonably necessary to carry out your instructions (for example, to place or manage orders, check order status, handle returns, or provide post-order tracking updates, where available); and * Acknowledge that the applicable Merchant or service provider's terms may also apply to your use of those credentials. **Email Accounts (OAuth Tokens).** If you choose to connect an email account (such as Gmail) to the Services, you authorize Henry to access that account only via the permissions you grant through the applicable authorization flow (for example, OAuth). We do not require or request your email password for this purpose. **Scope Limitation.** Where available, we will request and use the minimum permissions reasonably necessary to provide the feature you enable. For example, if you enable post-order tracking, we may access your connected email account to identify order confirmations and shipping updates and to provide related order status updates. Henry will not share your Access Credentials with third parties except as needed to provide the Services or as required by law. You are responsible for maintaining the security of your Access Credentials and for the actions we take using them at your direction. ### **4.4 Security and Responsibility** You are responsible for: * Keeping your Account credentials (username, password, API keys, tokens) confidential; * All activities that occur under your Account; and * Notifying us promptly at [philip@henrylabs.ai](mailto:philip@henrylabs.ai) if you suspect unauthorized access or a security breach involving your Account or Access Credentials. Henry is not liable for losses arising from unauthorized use of your Account or Access Credentials if you fail to keep them secure or to notify us promptly. ### **4.5 Cancellation** You may request to cancel your Account at any time by contacting [philip@henrylabs.ai](mailto:philip@henrylabs.ai) or using any self-service tools we provide. Upon cancellation, we may delete or de-identify your Account information in accordance with our data retention practices and applicable law. We are not responsible for any loss you may suffer as a result of cancellation or deletion. *** ## **5. User Content** ### **5.1 User Content Definition** "**User Content**" means any data, text, images, messages, instructions, prompts, or other content that you submit, upload, transmit, or otherwise make available to or through the Services, other than Feedback (Section 12) and Customer Data (Section 10). ### **5.2 License to Henry** You retain ownership of your User Content. However, by submitting User Content, you grant Henry a worldwide, non-exclusive, royalty-free, fully paid, transferable, and sublicensable license to use, host, copy, store, display, perform, modify, create derivative works from, and otherwise process your User Content: * To operate, maintain, and provide the Services; * To route or fulfill your orders with Merchants; * To prevent fraud, abuse, and misuse; * To analyze and improve the Services, including through logging and analytics; and * To comply with law and protect our rights. We will handle personal data contained in User Content in accordance with our Privacy Policy and any applicable Customer Agreement. ### **5.3 Responsibility for User Content** You are solely responsible for your User Content and represent and warrant that: * You have all necessary rights to submit and license your User Content as described above; * Your User Content does not infringe or violate any third-party rights or any law; and * Your User Content complies with these Terms, including the Acceptable Use rules in Section 6. Henry is not obligated to monitor User Content, but we may remove or disable access to any User Content that we believe violates these Terms or applicable law. *** ## **6. Acceptable Use** You agree not to (and not to permit or enable others to): 1. Use the Services for any fraudulent, deceptive, or illegal purpose; 2. Impersonate any person or entity or misrepresent your identity or affiliation; 3. Send, upload, or transmit any content that is unlawful, harmful, abusive, harassing, defamatory, obscene, or otherwise objectionable; 4. Use the Services to send unsolicited or unauthorized advertising, promotional materials, spam, or other forms of solicitation; 5. Copy, reproduce, publicly display, mirror, or frame any part of the Services without our express written permission, except as necessary for their intended use; 6. Attempt to reverse engineer, decompile, disassemble, or otherwise attempt to derive source code or underlying ideas from any part of the Services, except where such restrictions are prohibited by law; 7. Use the Services or any data obtained from the Services to create, train, or improve a service or product that competes, directly or indirectly, with Henry or the Services, or to benchmark the Services in a manner that is competitive with Henry; 8. Scrape, crawl, or harvest any data from the Services (by automated or manual means) except through documented and authorized APIs and in accordance with our instructions; 9. Interfere with or disrupt the operation of the Services, including by overloading, flooding, or attacking Henry's infrastructure, or by circumventing security or access controls; 10. Attempt to gain unauthorized access to any non-public portion of the Services, other users' accounts, or any systems or networks connected to the Services; 11. Use the Services or any data from the Services to operate your own independent automated system that targets Merchants in a manner that is unlawful or clearly designed to degrade or bypass the Merchant's security or anti-bot measures; 12. Collect or store personal information about other users without their consent, except as permitted by law and our Privacy Policy; 13. Use the Services for any purpose not expressly allowed by these Terms; 14. Use Order Tracking Data or other data derived from connected email accounts for advertising, resale, marketing profiling, or any purpose not directly related to providing user-facing order tracking features requested by the user. Henry may investigate and take any action we deem appropriate (including account suspension or termination and reporting to law enforcement) if we believe you have violated this Section. *** ## **7. Confidentiality** In your use of the Services, you may access information that is confidential or proprietary to Henry, Customers, or Merchants ("**Confidential Information**"). Confidential Information includes, without limitation: * Non-public technical and business information about the Services; * Customer or Merchant lists, order data, or transaction details; * Messages or instructions transmitted through the Services; and * Any information marked or reasonably understood to be confidential. Confidential Information does not include information that: 1. Was already lawfully known to you without confidentiality obligations before you received it; 2. Becomes publicly available without your breach of these Terms; or 3. Is received from a third party with the right to disclose it without confidentiality restrictions. You agree to: * Use Confidential Information solely for your authorized use of the Services; * Not disclose Confidential Information to any third party, except as required by law or with our prior written consent; and * Use reasonable safeguards to protect Confidential Information from unauthorized use, access, or disclosure. If you are compelled by law to disclose Confidential Information, you may do so, provided that you give Henry reasonable prior notice (to the extent legally permitted) so we may seek protective measures. *** ## **8. Export Controls and Sanctions** The Services and any software or technology provided by Henry may be subject to U.S. and other export control and sanctions laws. You agree that you will not: * Access or use the Services from any country or region subject to comprehensive U.S. sanctions, or by or on behalf of any person or entity listed on U.S. or other applicable sanctions or denied-party lists; or * Export, re-export, or transfer any part of the Services in violation of applicable export or sanctions laws. You are responsible for complying with all export, import, and sanctions laws and regulations that apply to your use of the Services. *** ## **9. Payments and Transactions** ### **9.1 Merchant of Record; Allocation of Responsibility** The Merchant of Record for a transaction is determined as described in Section 2.3. Who is responsible for your purchase depends on who that party is. **(a) Where a Merchant or other third party is the Merchant of Record.** Your purchase is between you and that party. It charges your payment method and is responsible for: * Pricing and taxes; * Product descriptions and quality; * Inventory, shipping, and delivery; * Returns, exchanges, and refunds; and * Compliance with consumer protection and other laws. In this case, Henry is not a party to the contract of sale and does not control whether that party honors its obligations. We may or may not have a direct commercial relationship with that party with respect to your individual order, and, to the fullest extent permitted by law, we are not liable to you for any obligations arising out of or related to that party's relationship with you, including its performance or non-performance of the sale, fulfillment, customer service, or any other obligations. **(b) Where Henry or a Henry affiliate is the Merchant of Record.** For that transaction, Henry or the applicable affiliate sells the product or service to you as a principal, charges your payment method, and is responsible for: * The contract of sale for that transaction; * Collecting and remitting applicable sales, use, or similar transaction taxes; * Order confirmation, billing, and transaction records; and * Returns, exchanges, refunds, and cancellations, in accordance with the return and refund policy we make available to you at or before checkout. Even where Henry or a Henry affiliate is the Merchant of Record, the underlying products and services are manufactured and supplied, and in most cases shipped, by the applicable Merchant or its suppliers. Henry does not manufacture those products and gives no warranty in respect of them beyond any warranty required by applicable law or expressly stated in writing at the time of sale. Product warranties, where offered, are provided by the manufacturer or the applicable Merchant. ### **9.2 Payment Authorization** When you submit an order through the Services, you authorize Henry to: * Transmit your payment information to the applicable Merchant of Record or its payment processor, or, where Henry or a Henry affiliate is the Merchant of Record, process that payment through our own payment processors; * Receive relevant status information about your transaction (for example, confirmation, failure, refund status); and * Display or relay such information back to you or to a Customer application. Payment methods are subject to approval by the relevant issuer and payment processor. Henry has no responsibility for payment authorization or declines. ### **9.3 Refunds, Chargebacks, and Disputes** Refunds and returns are subject to the policies of the applicable Merchant of Record. You are responsible for reviewing the applicable terms, return policy, and privacy policy before completing a transaction. Where a Merchant or other third party is the Merchant of Record, Henry does not control, and is not responsible for: * Whether that party grants a refund or exchange; * How that party handles chargebacks or disputes; or * Any errors by payment processors or financial institutions. In that case, you should direct any questions or disputes about a specific order to that party and/or your payment provider. Where Henry or a Henry affiliate is the Merchant of Record, direct questions and disputes about that order to us using the contact details in Section 24, and we will handle them in accordance with the return and refund policy made available to you at or before checkout and with applicable law. ### **9.4 Your Statutory Rights** Nothing in these Terms limits or excludes any right or remedy you have under mandatory consumer protection law that cannot be limited or excluded by agreement. Where Henry or a Henry affiliate is the Merchant of Record, those rights apply to us in respect of that transaction. *** ## **10. Customer Data** If a Customer has invited or authorized you to use the Services under its account or integration, the Customer may submit or generate content, data, or information within the Services ("**Customer Data**"). As between Henry and the Customer, and to the extent stated in the applicable Customer Agreement: * Customer Data is owned or controlled by the Customer; and * Henry processes Customer Data on behalf of the Customer. You acknowledge and agree that it is solely the Customer's responsibility to: 1. Inform you of its policies and practices that affect Customer Data; 2. Obtain any rights, permissions, or consents necessary to collect and use Customer Data (including personal data); 3. Ensure that Customer Data can be lawfully processed by Henry as described in the Customer Agreement and our Privacy Policy; and 4. Address and resolve any disputes with you relating to Customer Data or the Customer's use of the Services. **Order Tracking Data Shared with Customers.** If you use the Services through a Customer integration and you connect an email account for order tracking, you direct Henry to provide the Customer with Order Tracking Data (for example, shipment status events) so the Customer can display order updates to you. The Customer may use Order Tracking Data only to provide or improve end-user order features and may not use it for advertising, marketing profiling, resale, or any purpose unrelated to the user-facing order experience. Subject to the Customer Agreement, the Services are provided to you on an "AS IS" and "AS AVAILABLE" basis, without warranties of any kind. *** ## **11. Developer / API / SDK Terms** If you access the Services via APIs, SDKs, or other developer tools: * You must keep your API keys and tokens secure and not embed them in client-side code or otherwise expose them publicly; * You must comply with any rate limits, quotas, or usage guidelines that we specify, and we may throttle or suspend access that exceeds or abuses those limits; * You may not resell, sublicense, or white-label the Services without Henry's prior written consent; and * You are responsible for the security of your systems and for any use of the Services through your API keys or integrations; * If you receive Order Tracking Data via webhooks, APIs, or SDKs, you will: (i) store it securely; (ii) use it only to provide user-facing order status and related features requested by the end user; and (iii) not use it for advertising, marketing profiling, resale, or any purpose unrelated to the user-facing order experience. We may modify or discontinue APIs and SDKs at any time, and unless otherwise agreed in writing, we do not provide uptime or performance guarantees. *** ## **12. Feedback** If you provide Henry with suggestions, ideas, proposals, or other feedback about the Services ("**Feedback**"), you do so voluntarily, and you acknowledge that Henry may use such Feedback without obligation to you. You grant Henry a perpetual, irrevocable, worldwide, non-exclusive, transferable, sublicensable, royalty-free license to use, copy, modify, distribute, display, and create derivative works from such Feedback for any lawful purpose, without compensation to you. You represent and warrant that you have all necessary rights to provide the Feedback and grant this license. *** ## **13. Intellectual Property** Henry and its licensors own all right, title, and interest in and to the Services, including all software, code, interfaces, designs, text, graphics, logos, and other content, and all related intellectual property rights. Except for the limited license granted in Section 4.2, nothing in these Terms transfers any ownership rights in the Services to you or grants you any rights to use Henry's trademarks, logos, or branding. You agree not to remove, alter, or obscure any copyright, trademark, or other proprietary notices on or in the Services. *** ## **14. DMCA and IP Complaints** Henry respects the intellectual property rights of others and expects users to do the same. It is our policy, in appropriate circumstances, to disable or terminate accounts of users who repeatedly infringe others' copyrights. If you believe content on the Services infringes your copyright, you may submit a DMCA notice to our designated agent ("**Copyright Agent**") with: 1. A description of the copyrighted work you claim has been infringed, or a representative list; 2. Identification of the material you claim is infringing and information reasonably sufficient to locate it (for example, a URL); 3. Your name, mailing address, telephone number, and email address; 4. Both of the following statements: * "I have a good-faith belief that the use of the copyrighted material described in this notice is not authorized by the copyright owner, its agent, or the law." * "The information in this notice is accurate, and under penalty of perjury, I am the copyright owner or am authorized to act on behalf of the owner of an exclusive right that is allegedly infringed." 5. Your physical or electronic signature. Send your DMCA notice to: **Henry Social, Inc.**\ Attn: Copyright Agent\ 265 Lorimer St 401\ New York, NY, 11206 US\ Email: [philip@henrylabs.ai](mailto:philip@henrylabs.ai) We may also process reasonable trademark or other IP complaints submitted to this address. *** ## **15. Privacy and Data Transfers** Our Privacy Policy explains how Henry collects, uses, and shares information about you. By using the Services, you consent to our data practices as described in the Privacy Policy. The Services may involve processing and storing information in the United States and other countries, which may have different data protection laws than your jurisdiction. Where required, we will implement appropriate safeguards for cross-border data transfers. If you interact with a Merchant through the Services, the Merchant's own privacy policy and terms will govern that Merchant's use of your information, and Henry is not responsible for any Merchant's privacy, security, or data practices. Where Henry or a Henry affiliate is the Merchant of Record, our Privacy Policy governs our handling of the personal data we process to complete that transaction. ### **15A. Email Order Tracking Data (Including Connected Email Accounts)** **(a) Purpose.** If you connect an email account to the Services, Henry will access and process messages solely to provide user-facing order features you enable, such as identifying order confirmations, shipment notifications, delivery status changes, delays/exceptions, cancellations, returns, refunds, and related post-purchase updates. **(b) Data Minimization.** Henry will use reasonable efforts to minimize the data accessed and stored from connected email accounts. We may process email content to extract limited order-related fields such as merchant name, order identifier, shipment status, carrier, tracking number, timestamps, and similar order-related metadata (collectively, "**Order Tracking Data**"). **(c) No Ads / No Sale / No Marketing Profiling.** Henry does not use data obtained from connected email accounts to serve advertisements (including personalized or retargeted ads), does not sell such data, and does not use such data for marketing profiling or for determining eligibility for credit, lending, or insurance. **(d) Limited Sharing / Transfers.** Henry will not transfer email content from connected email accounts to third parties except: (1) at your direction to provide the feature you enabled (for example, sending Order Tracking Data to the Customer application you are using so it can display order updates to you); (2) to service providers that process data on our behalf to operate the Services (subject to confidentiality and security obligations); (3) for security, fraud prevention, or abuse detection; or (4) to comply with law or a valid legal process. **(e) Human Access Controls.** Henry does not permit humans to read email content obtained through connected accounts except as necessary to provide user support requested by you, for security purposes (such as investigating abuse), or to comply with law. **(f) Control and Revocation.** You may disconnect a connected email account at any time through the Services (if available) or by contacting us. After disconnection, Henry will stop accessing that connected account and will handle retained Order Tracking Data in accordance with our Privacy Policy and retention practices. ### **15B. Payment Card Security** Henry is committed to maintaining appropriate administrative, technical, and physical safeguards to protect information processed through the Services. Where applicable, Henry will maintain compliance with the Payment Card Industry Data Security Standard ("PCI DSS") for the systems, infrastructure, and operational processes under Henry's control. Payment card processing is provided through third-party PCI DSS compliant payment service providers. Henry does not assume responsibility for the PCI DSS compliance obligations of such providers or for systems, applications, devices, or business processes outside Henry's control. You acknowledge that you are responsible for maintaining the security and compliance of any systems, applications, configurations, or business processes under your control when using the Services. Each party is responsible for fulfilling its respective obligations under the applicable shared responsibility model. **Logging and Incident Response Support.** Henry retains activity and system logs relating to use of the Services, and maintains logging appropriate to support incident investigation. Customers may request log records relevant to their own integration, including to support an incident response, by contacting [support@henrylabs.ai](mailto:support@henrylabs.ai). Henry verifies ownership of the requested records before release and provides only records belonging to the requesting Customer. Log retention follows Henry's documented retention practices and applicable PCI DSS requirements. *** ## **16. Third-Party Services** The Services may contain links or integrations to third-party websites, applications, services, or advertisements ("**Third-Party Services**"). Third-Party Services are not under Henry's control, and Henry is not responsible for them. Henry does not endorse or guarantee any Third-Party Services or any content, products, or services made available through them. Your use of Third-Party Services is at your own risk and subject to any terms and privacy policies of those third parties. *** ## **17. Indemnification** You agree to indemnify, defend, and hold harmless Henry and its officers, directors, employees, agents, affiliates, and licensors (each, an "**Indemnified Party**") from and against any and all claims, demands, losses, liabilities, damages, costs, and expenses (including reasonable attorneys' fees) arising out of or related to: 1. Your use of or access to the Services; 2. Any User Content or other information you submit to or through the Services; 3. Your violation of these Terms; or 4. Your violation of any applicable law, rule, or regulation. Henry may, at its own expense, assume exclusive defense and control of any matter otherwise subject to indemnification by you, in which case you agree to cooperate with us. This indemnity does not require you to indemnify any Indemnified Party for that party's own intentional misconduct, fraud, or other non-indemnifiable conduct under applicable law. *** ## **18. Disclaimers of Warranties** TO THE MAXIMUM EXTENT PERMITTED BY LAW, YOU UNDERSTAND AND AGREE THAT YOUR USE OF THE SERVICES IS AT YOUR SOLE RISK. THE SERVICES (INCLUDING ANY SOFTWARE, APIS, DASHBOARDS, HOSTED CHECKOUT PAGES, OR OTHER COMPONENTS) ARE PROVIDED "AS IS" AND "AS AVAILABLE," WITH ALL FAULTS, AND WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY. WITHOUT LIMITING THE FOREGOING, HENRY EXPRESSLY DISCLAIMS ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, AND ANY WARRANTIES ARISING FROM COURSE OF DEALING OR USAGE OF TRADE. HENRY DOES NOT WARRANT THAT: * THE SERVICES WILL BE UNINTERRUPTED, SECURE, OR ERROR-FREE; * ANY DEFECTS WILL BE CORRECTED; OR * THE RESULTS OF USING THE SERVICES (INCLUDING ANY ORDER OR TRANSACTION ROUTED TO A MERCHANT) WILL MEET YOUR EXPECTATIONS OR REQUIREMENTS. HENRY DOES NOT CONTROL, AND IS NOT RESPONSIBLE FOR, MERCHANTS OR THEIR PRODUCTS OR SERVICES, EXCEPT TO THE EXTENT HENRY OR A HENRY AFFILIATE IS THE MERCHANT OF RECORD FOR A TRANSACTION AND SECTION 9.1(b), SECTION 9.4, OR APPLICABLE LAW PROVIDES OTHERWISE. Some jurisdictions do not allow the exclusion of certain warranties, so some of the above exclusions may not apply to you. In that case, the warranties will be limited to the fullest extent permitted by applicable law. *** ## **19. Limitation of Liability** TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL HENRY BE LIABLE TO YOU OR ANY THIRD PARTY FOR: * ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES; OR * ANY LOSS OF PROFITS, REVENUE, GOODWILL, DATA, OR OTHER INTANGIBLE LOSSES; ARISING OUT OF OR IN CONNECTION WITH THESE TERMS OR YOUR USE OF (OR INABILITY TO USE) THE SERVICES, EVEN IF HENRY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES OR IF A LIMITED REMEDY FAILS OF ITS ESSENTIAL PURPOSE. TO THE MAXIMUM EXTENT PERMITTED BY LAW, HENRY'S TOTAL AGGREGATE LIABILITY FOR ALL CLAIMS ARISING OUT OF OR RELATED TO THESE TERMS OR THE SERVICES WILL NOT EXCEED THE GREATER OF: 1. THE AMOUNTS (IF ANY) PAID BY YOU TO HENRY FOR THE SERVICES DURING THE TWO (2) MONTHS IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE CLAIM; OR 2. ONE HUNDRED U.S. DOLLARS (US \$100). Some jurisdictions do not allow limitations of liability for incidental or consequential damages, so some of the above limitations may not apply to you. In such cases, Henry's liability will be limited to the maximum extent allowed by law. *** ## **20. Dispute Resolution and Arbitration Agreement** **PLEASE READ THIS SECTION CAREFULLY. IT AFFECTS YOUR LEGAL RIGHTS.** ### **20.1 Agreement to Arbitrate** You and Henry agree that any dispute, claim, or controversy arising out of or relating to these Terms, the Services, any communications between you and Henry, or any transactions involving the Services (collectively, "**Disputes**") will be resolved exclusively by binding arbitration on an individual basis, except that: * You may bring an eligible claim in small claims court, so long as it remains an individual action; and * Either party may seek injunctive or other equitable relief in court to prevent or stop actual or threatened infringement or misuse of intellectual property rights. This Section is the "**Arbitration Agreement**." This Arbitration Agreement is intended to be interpreted broadly and includes Disputes that arose before you accepted these Terms. ### **20.2 Exceptions for Non-U.S. Consumers** If you are a consumer resident in a jurisdiction where mandatory law does not permit arbitration or prohibits certain aspects of this Arbitration Agreement, then this Section 20 may not apply to you to the extent it conflicts with such mandatory law. In that case, the governing law and venue provisions in Section 21 will still apply to the extent permitted. ### **20.3 Arbitration Rules and Forum** This Arbitration Agreement is governed by the U.S. Federal Arbitration Act. The arbitration will be administered by JAMS under its applicable rules (including the Streamlined Arbitration Rules for qualifying claims and the Comprehensive Arbitration Rules for others), as modified by this Arbitration Agreement. You can find JAMS rules at [http://www.jamsadr.com](http://www.jamsadr.com) or by calling 1-800-352-5267. The arbitration may be conducted: * In person in New York County, New York, * In the county where you reside, * By telephone or video conference, or * Based solely on written submissions, as determined by the arbitrator. JAMS's rules will govern allocation of arbitration fees. If the arbitrator determines that you cannot afford JAMS's fees and cannot obtain a waiver, Henry will pay them. Henry may also voluntarily pay certain fees in its discretion. ### **20.4 Arbitrator's Authority** The arbitrator will have exclusive authority to resolve all Disputes covered by this Arbitration Agreement, including questions about its interpretation, scope, or enforceability, except that a court will decide any question about the enforceability of the class action waiver in Section 20.5. The arbitrator may award the same individual relief that a court could award, consistent with these Terms and applicable law. The arbitrator will issue a written decision stating the essential findings and conclusions. ### **20.5 Waiver of Class Actions** YOU AND HENRY AGREE THAT DISPUTES WILL BE RESOLVED ONLY ON AN INDIVIDUAL BASIS AND NOT IN A CLASS, COLLECTIVE, REPRESENTATIVE, OR MASS ACTION. YOU AND HENRY WAIVE ANY RIGHT TO PARTICIPATE IN A CLASS OR REPRESENTATIVE ACTION, OR TO HAVE A DISPUTE HEARD AS A CLASS ARBITRATION OR CLASS ACTION. If this class action waiver is found unenforceable with respect to a particular claim, that claim (and only that claim) must be brought in court, not arbitration. ### **20.6 Waiver of Jury Trial** YOU AND HENRY WAIVE ANY CONSTITUTIONAL OR STATUTORY RIGHT TO A TRIAL BY JURY. DISPUTES WILL BE RESOLVED BY A NEUTRAL ARBITRATOR, NOT A JUDGE OR JURY. ### **20.7 Opt-Out** You may opt out of this Arbitration Agreement by sending a written notice to [philip@henrylabs.ai](mailto:philip@henrylabs.ai) within thirty (30) days after you first become subject to this Arbitration Agreement (for example, the first time you accept these Terms). Your opt-out notice must include: * Your full name; * Your mailing address; * The email address associated with your Account; and * A clear statement that you wish to opt out of the Arbitration Agreement in these Terms. If you opt out, this Arbitration Agreement will not apply to you. All other provisions of these Terms remain in effect. *** ## **21. Governing Law and Venue** These Terms and any Disputes are governed by the laws of the State of New York, without regard to its conflict-of-law principles, except that the Federal Arbitration Act governs the enforcement of the Arbitration Agreement in Section 20. To the extent these Terms allow for litigation in court (for example, if you opt out of arbitration or for claims not subject to arbitration), you and Henry agree that: * All such claims must be brought exclusively in the state or federal courts located in New York County, New York; and * You and Henry consent to the personal jurisdiction of those courts. *** ## **22. Termination** These Terms remain in effect while you use the Services. We may, in our sole discretion and without liability to you, suspend, restrict, or terminate your access to all or part of the Services (including by blocking certain IP addresses or disabling your Account) at any time, including if we believe you have violated these Terms or any applicable law. If your Account is terminated or suspended, you may not create a new account without our express written permission. Termination does not limit any of Henry's other rights or remedies under these Terms or at law. *** ## **23. General Provisions** **No Agency or Partnership.** These Terms do not create any partnership, joint venture, employment, or agency relationship between you and Henry or between you and any Merchant. **Electronic Communications and Signatures.** By using the Services, you consent to receive electronic communications from Henry and agree that all agreements, notices, and other communications we provide electronically satisfy any legal requirement that such communications be in writing. You consent to the use of electronic signatures and records for all transactions related to the Services. **No Third-Party Beneficiaries.** Except as expressly stated, no person or entity other than you and Henry has any rights under these Terms. **Entire Agreement.** These Terms, together with any applicable Customer Agreement and our Privacy Policy, constitute the entire agreement between you and Henry regarding the Services and supersede all prior or contemporaneous agreements or understandings. **Severability.** If any provision of these Terms is found invalid or unenforceable, the remaining provisions will continue in full force and effect. **No Waiver.** Any failure by Henry to enforce any right or provision of these Terms is not a waiver of such right or provision. **Force Majeure.** Henry will not be liable for any delay or failure to perform resulting from causes beyond its reasonable control, including acts of God, natural disasters, war, terrorism, labor disputes, or internet or telecommunications failures. **Assignment.** You may not assign or transfer these Terms or any rights or obligations under them, by operation of law or otherwise, without Henry's prior written consent. Henry may freely assign these Terms. Subject to the foregoing, these Terms are binding upon and inure to the benefit of the parties and their respective successors and permitted assigns. **Interpretation.** These Terms will not be construed against Henry because Henry drafted them. *** ## **24. Contact Information** If you have questions about the Services or these Terms, or wish to file a complaint, you may contact us at: **Henry Social, Inc.** Attn: Privacy Officer 265 Lorimer St 401 New York, NY, 11206 US Email: **[philip@henrylabs.ai](mailto:philip@henrylabs.ai)** *** © **2025 Henry Social, Inc.** All rights reserved. # Test Cards Source: https://docs.henrylabs.ai/v1/info/test-cards Test card numbers and testing guidelines for Henry Checkout ## Overview When testing your Henry Shopping API integration, use these test card numbers to simulate different payment scenarios. ## Cards by brand Use these test card numbers to test successful payments with different card brands: | Brand | Number | CVC | Date | | :- | :- | :- | :- | | Visa | `4242 4242 4242 4242` | Any 3 digits | Any future date | | Visa (debit) | `4000 0566 5566 5556` | Any 3 digits | Any future date | | Mastercard | `5555 5555 5555 4444` | Any 3 digits | Any future date | | Mastercard (2-series) | `2223 0031 2200 3222` | Any 3 digits | Any future date | | Mastercard (debit) | `5200 8282 8282 8210` | Any 3 digits | Any future date | | Mastercard (prepaid) | `5105 1051 0510 5100` | Any 3 digits | Any future date | | American Express | `3782 8224 6310 005` | Any 4 digits | Any future date | | American Express | `3714 4963 5398 431` | Any 4 digits | Any future date | | Discover | `6011 1111 1111 1117` | Any 3 digits | Any future date | | Discover | `6011 0009 9013 9424` | Any 3 digits | Any future date | | Discover (debit) | `6011 9811 1111 1113` | Any 3 digits | Any future date | | Diners Club | `3056 9300 0902 0004` | Any 3 digits | Any future date | | Diners Club (14-digit) | `3622 7206 2716 67` | Any 3 digits | Any future date | | BCcard/DinaCard | `6555 9000 0060 4105` | Any 3 digits | Any future date | | JCB | `3566 0020 2036 0505` | Any 3 digits | Any future date | | UnionPay | `6200 0000 0000 005` | Any 3 digits | Any future date | | UnionPay (debit) | `6200 0000 0000 047` | Any 3 digits | Any future date | | UnionPay (19-digit) | `6205 5000 0000 0000 004` | Any 3 digits | Any future date | ## International cards Use these test card numbers to test payments from specific countries and regions: ### Americas | Country | Number | Brand | | :- | :- | :- | | United States (US) | `4242 4242 4242 4242` | Visa | | Argentina (AR) | `4000 0003 2000 0021` | Visa | | Brazil (BR) | `4000 0007 6000 0002` | Visa | | Canada (CA) | `4000 0012 4000 0000` | Visa | | Chile (CL) | `4000 0015 2000 0001` | Visa | | Colombia (CO) | `4000 0017 0000 0003` | Visa | | Costa Rica (CR) | `4000 0018 8000 0005` | Visa | | Ecuador (EC) | `4000 0021 8000 0000` | Visa | | Mexico | `4000 0048 4000 8001` | Visa | | Mexico | `5062 2100 0000 0009` | Carnet | | Panama (PA) | `4000 0059 1000 0000` | Visa | | Paraguay (PY) | `4000 0060 0000 0066` | Visa | | Peru (PE) | `4000 0060 4000 0068` | Visa | | Uruguay (UY) | `4000 0085 8000 0003` | Visa | ### Europe and Middle East | Country | Number | Brand | | :- | :- | :- | | United Arab Emirates | `4000 0078 4000 0001` | Visa | | United Arab Emirates | `5200 0078 4000 0022` | Mastercard | | Austria (AT) | `4000 0004 0000 0008` | Visa | | Belgium (BE) | `4000 0005 6000 0004` | Visa | | Bulgaria (BG) | `4000 0010 0000 0000` | Visa | | Belarus (BY) | `4000 0011 2000 0005` | Visa | | Croatia (HR) | `4000 0019 1000 0009` | Visa | | Cyprus (CY) | `4000 0019 6000 0008` | Visa | | Czech Republic (CZ) | `4000 0020 3000 0002` | Visa | | Denmark (DK) | `4000 0020 8000 0001` | Visa | | Estonia (EE) | `4000 0023 3000 0009` | Visa | | Finland (FI) | `4000 0024 6000 0001` | Visa | | France (FR) | `4000 0025 0000 0003` | Visa | | Germany (DE) | `4000 0027 6000 0016` | Visa | | Gibraltar (GI) | `4000 0029 2000 0005` | Visa | | Greece (GR) | `4000 0030 0000 0030` | Visa | | Hungary (HU) | `4000 0034 8000 0005` | Visa | | Ireland (IE) | `4000 0037 2000 0005` | Visa | | Italy (IT) | `4000 0038 0000 0008` | Visa | | Latvia (LV) | `4000 0042 8000 0003` | Visa | | Liechtenstein (LI) | `4000 0043 8000 0001` | Visa | | Lithuania (LT) | `4000 0044 0000 0000` | Visa | | Luxembourg (LU) | `4000 0044 2000 0006` | Visa | | Malta (MT) | `4000 0047 0000 0007` | Visa | | Netherlands (NL) | `4000 0052 8000 0008` | Visa | | Norway (NO) | `4000 0057 8000 0007` | Visa | | Poland (PL) | `4000 0061 6000 0005` | Visa | | Portugal (PT) | `4000 0062 0000 0002` | Visa | | Romania (RO) | `4000 0064 2000 0001` | Visa | | Slovakia (SK) | `4000 0070 3000 0001` | Visa | | Slovenia (SI) | `4000 0070 5000 0006` | Visa | | Spain (ES) | `4000 0072 4000 0007` | Visa | | Sweden (SE) | `4000 0075 2000 0008` | Visa | | Switzerland (CH) | `4000 0075 6000 0009` | Visa | | United Kingdom (GB) | `4000 0082 6000 0000` | Visa | | United Kingdom (debit) | `4000 0582 6000 0005` | Visa (debit) | | United Kingdom | `5555 5582 8555 5557` | Mastercard | ### Asia Pacific | Country | Number | Brand | | :- | :- | :- | | Australia (AU) | `4000 0003 6000 0006` | Visa | | China (CN) | `4000 0015 6000 0002` | Visa | | Hong Kong (HK) | `4000 0034 4000 0004` | Visa | | India (IN) | `4000 0035 6000 0008` | Visa | | Indonesia (ID) | `4000 0036 0000 0000` | Visa | | Japan (JP) | `4000 0039 2000 0003` | Visa | | Japan | `3530 1113 3330 0000` | JCB | | Malaysia (MY) | `4000 0045 8000 0002` | Visa | | New Zealand (NZ) | `4000 0055 4000 0008` | Visa | | Philippines (PH) | `4000 0060 8000 0001` | Visa | | Singapore (SG) | `4000 0070 2000 0003` | Visa | | South Korea (KR) | `4000 0041 0000 0006` | Visa | | Taiwan (TW) | `4000 0015 8000 0002` | Visa | | Thailand (TH) | `4000 0076 4000 0003` | Visa | | Vietnam (VN) | `4000 0070 4000 0000` | Visa | # Card Element Source: https://docs.henrylabs.ai/v1/sdk/client/elements/card-element Securely collect payment card details in an isolated iframe and tokenize them for server-side processing. The Card Element renders a secure card input form inside an iframe. Card data never touches your page - it's collected entirely within the iframe, then tokenized. You receive a short-lived `cardToken` to pass to your server for payment processing. This element is the standard way to collect card details in a [Headless Checkout](/v1/sdk/server/guides/checkout#headless-checkout) flow. Because the card is captured and tokenized inside the iframe, raw card data never reaches your servers - this keeps you out of PCI DSS scope. A server-side `POST /card/tokenize` REST endpoint also exists (it is not exposed through the SDK), but passing the raw PAN, CVV, and expiry through your own backend brings your systems into PCI scope (typically SAQ D). Prefer this element unless you already run a PCI-compliant environment. *** ## Usage ```js theme={null} import Henry from '@henrylabs/js'; const cardEl = Henry.createCardElement('#card-container'); ``` Add a container in your HTML: ```html theme={null}
``` To submit, call `validate()` then `submit()`: ```js theme={null} const isValid = await cardEl.validate(); if (isValid) { const { cardToken } = await cardEl.submit(); // send cardToken to the server SDK } ``` *** ## Options ### `styles` `CardStyles` Optional. Customize the appearance of the card input fields. ```js theme={null} const cardEl = Henry.createCardElement('#card-container', { styles: { cardNumber: { base: { color: '#111', fontSize: '16px', fontFamily: 'Inter, sans-serif', }, '::placeholder': { color: '#aaa', }, invalid: { color: '#e53e3e', }, }, cardExpiration: { base: { color: '#111' }, }, cardVerification: { base: { color: '#111' }, }, }, }); ``` #### `CardStyles` reference | Field | Type | Description | | - | - | - | | `cardNumber` | `CardElementStyle` | Styles for the card number field | | `cardExpiration` | `CardElementStyle` | Styles for the expiration date field | | `cardVerification` | `CardElementStyle` | Styles for the CVV/CVC field | | `container` | `CSSProperties` | Styles applied to the outer container element | | `inputWrappers` | `CSSProperties` | Styles applied to each field wrapper | | `section1` | `CSSProperties` | Styles applied to the first row (card number) | | `section2` | `CSSProperties` | Styles applied to the second row (expiry + CVV) | | `fonts` | `string[]` | Font URLs to load inside the iframe | #### `CardElementStyle` reference Each field style (`cardNumber`, `cardExpiration`, `cardVerification`) accepts: | Field | Type | Description | | - | - | - | | `base` | `CardElementStyleVariant` | Default state styles | | `complete` | `CardElementStyleVariant` | Styles when the field is filled and valid | | `invalid` | `CardElementStyleVariant` | Styles when the field contains an error | | `empty` | `CardElementStyleVariant` | Styles when the field is empty | | `container` | `CardElementStyleVariant` | Styles for the field's inner container | | `placeholder` | `string` | Custom placeholder text | | `label` | `string` | Custom label text | | `labelContainer` | `CSSProperties` | Styles for the label element | | `fonts` | `string[]` | Field-specific font URLs | `CardElementStyleVariant` supports these CSS properties: ```ts theme={null} { backgroundColor?: string color?: string fontFamily?: string fontSize?: string fontStyle?: string fontWeight?: string | number letterSpacing?: string lineHeight?: string | number padding?: string textAlign?: string textDecoration?: string textShadow?: string textTransform?: string // Pseudo-class overrides ":hover"?: { ... } ":focus"?: { ... } ":read-only"?: { ... } "::placeholder"?: { ... } "::selection"?: { ... } ":disabled"?: { ... } } ``` *** ## Methods ### `validate()` → `Promise` Returns `true` if all card fields contain valid input. Use this before submitting to surface validation errors to the buyer. ```js theme={null} const isValid = await cardEl.validate(); if (!isValid) { // show your own error message } ``` ### `submit()` → `Promise<{ cardToken: string }>` Tokenizes the card and returns a `cardToken`. Pass this token to your server to complete the payment - never send raw card data from your frontend. ```js theme={null} const { cardToken } = await cardEl.submit(); await fetch('/api/pay', { method: 'POST', body: JSON.stringify({ cardToken }), headers: { 'Content-Type': 'application/json' }, }); ``` ### `destroy()` Removes the iframe and cleans up all event listeners. Call this when navigating away or unmounting a component. ```js theme={null} cardEl.destroy(); ``` *** ## Full example ```html theme={null}
``` *** ## React ```tsx theme={null} import { useEffect, useRef } from 'react'; import Henry from '@henrylabs/js'; function CardForm() { const cardElRef = useRef | null>(null); const initialized = useRef(false); useEffect(() => { if (initialized.current) return; initialized.current = true; cardElRef.current = Henry.createCardElement('#card-container', { styles: { cardNumber: { base: { color: '#111', fontSize: '16px' }, }, }, }); return () => cardElRef.current?.destroy(); }, []); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!cardElRef.current) return; const isValid = await cardElRef.current.validate(); if (!isValid) return; const { cardToken } = await cardElRef.current.submit(); // send cardToken to the server SDK } return (
); } ``` *** ## Related Embed a complete buy-now checkout button and modal Build headless checkout flows server-side # CVC Element Source: https://docs.henrylabs.ai/v1/sdk/client/elements/cvc-element Re-collect the security code for a stored card in an isolated iframe, without re-entering the full card. The CVC Element renders a single secure CVC/CVV input inside an iframe and applies the value to a card you already tokenized. The buyer re-enters three or four digits; the card number and expiry stay where they are and are never re-collected. You keep the same `cardToken` throughout - the element patches the stored card in place rather than creating a new one. ## Why this is needed A card verification code can't be stored, so the CVC captured when you tokenized a card expires while the rest of the card stays valid. Charging a stored card after that point requires a fresh CVC from the buyer. This element is how you collect it. A purchase attempted after the code has lapsed fails fast — `cart.checkout.purchase` returns a `400` asking you to re-collect it. The CVC is captured and applied inside the iframe, so it never reaches your servers - this keeps you out of PCI DSS scope. A server-side `POST /card/{cardToken}/cvv` REST endpoint also exists (it is not exposed through the SDK) for backends that can't run an iframe, but passing the raw CVV through your own backend brings your systems into PCI scope. Prefer this element unless you already run a PCI-compliant environment. *** ## Usage ```js theme={null} import Henry from '@henrylabs/js'; const cvcEl = Henry.createCvcElement('#cvc-container', { cardToken: 'card_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', }); ``` Add a container in your HTML: ```html theme={null}
``` The `cardToken` is the token you received from a previous `cardEl.submit()` call - see the [Card Element](/v1/sdk/client/elements/card-element). To apply the new code, call `validate()` then `submit()`: ```js theme={null} const isValid = await cvcEl.validate(); if (isValid) { await cvcEl.submit(); // the existing cardToken is now chargeable again } ``` *** ## Options ### `cardToken` `string` Required. The card token identifying the stored card to update. ### `styles` `CvcStyles` Optional. Customize the appearance of the CVC input. ```js theme={null} const cvcEl = Henry.createCvcElement('#cvc-container', { cardToken: 'card_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', styles: { cardVerification: { base: { color: '#111', fontSize: '16px', fontFamily: 'Inter, sans-serif', }, '::placeholder': { color: '#aaa', }, invalid: { color: '#e53e3e', }, }, }, }); ``` #### `CvcStyles` reference | Field | Type | Description | | - | - | - | | `cardVerification` | `CardElementStyle` | Styles for the CVV/CVC field | | `container` | `CSSProperties` | Styles applied to the outer container element | | `inputWrappers` | `CSSProperties` | Styles applied to the field wrapper | | `fonts` | `string[]` | Font URLs to load inside the iframe | `cardVerification` accepts the same `CardElementStyle` shape documented under the [Card Element](/v1/sdk/client/elements/card-element#cardelementstyle-reference) - `base`, `complete`, `invalid`, `empty`, `container`, `placeholder`, `label`, `labelContainer`, `fonts`, and the pseudo-class overrides. *** ## Methods ### `validate()` → `Promise` Returns `true` when the field contains a valid code. Use this to gate your submit button. ```js theme={null} const isValid = await cvcEl.validate(); if (!isValid) { // show your own error message } ``` ### `submit()` → `Promise` Applies the entered code to the stored card. Resolves on success and rejects with the failure reason otherwise. The `cardToken` is unchanged - after this resolves you can charge it again. ```js theme={null} await cvcEl.submit(); ``` ### `destroy()` Removes the iframe and cleans up all event listeners. Call this when navigating away or unmounting a component. ```js theme={null} cvcEl.destroy(); ``` *** ## Full example ```html theme={null}
``` *** ## React ```tsx theme={null} import { useEffect, useRef } from 'react'; import Henry from '@henrylabs/js'; function CvcConfirm({ cardToken }: { cardToken: string }) { const cvcElRef = useRef | null>(null); const initialized = useRef(false); useEffect(() => { if (initialized.current) return; initialized.current = true; cvcElRef.current = Henry.createCvcElement('#cvc-container', { cardToken, styles: { cardVerification: { base: { color: '#111', fontSize: '16px' }, }, }, }); return () => cvcElRef.current?.destroy(); }, [cardToken]); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!cvcElRef.current) return; const isValid = await cvcElRef.current.validate(); if (!isValid) return; await cvcElRef.current.submit(); // charge the same cardToken from your server } return (
); } ``` *** ## Related Collect and tokenize a full card Build headless checkout flows server-side # Introduction Source: https://docs.henrylabs.ai/v1/sdk/client/getting-started/introduction Embed checkout and card collection directly into your frontend with a single script The **Henry JS SDK** gives your frontend a complete checkout experience out of the box. Drop a fully managed checkout widget into any page in seconds, or embed a secure card input for custom payment flows - all from a lightweight, framework-agnostic library. Embed a complete buy-now checkout button and modal that handles cart, payment, and order confirmation Securely collect card details in an isolated iframe and tokenize them for server-side processing Re-collect the security code for a stored card without re-entering the full card *** ## Get started in minutes ```bash npm theme={null} npm install @henrylabs/js ``` ```bash bun theme={null} bun add @henrylabs/js ``` ```bash pnpm theme={null} pnpm add @henrylabs/js ``` ```bash yarn theme={null} yarn add @henrylabs/js ``` Or load it directly from the CDN - no build step required: ```html theme={null} ``` When loaded via CDN, the SDK is available as `window.Henry`. Go to the [Henry Dashboard](https://app.henrylabs.ai/dashboard/settings/app/developer) and copy your Widget ID. You'll pass this to `createCheckoutElement`. ```html theme={null}
```
*** ## How it works The Henry JS SDK renders UI elements inside your page using lightweight iframes. This keeps sensitive data (card numbers, payment info) fully isolated from your application while letting you control placement, styling, and events. * **Card Element** - renders a secure card input iframe. Call `submit()` to tokenize the card and receive a `cardToken` you can pass to your server. * **CVC Element** - renders a single secure CVC input for a card you already tokenized. A security code can't be stored, so it expires while the rest of the card stays valid; this applies a fresh one to the existing `cardToken` without re-collecting the card number or expiry. * **Checkout Widget** - renders a button (full, compact, comfortable, or bubble) that opens Henry's hosted checkout modal on click. You supply the product links; Henry handles cart creation, payment, and order tracking. The Checkout Widget requires a **Widget ID** from the [Henry Dashboard](https://app.henrylabs.ai/dashboard/settings/app/developer). *** ## Explore the guides End-to-end walkthrough - install, mount, and handle checkout events All options, display modes, and events for the checkout widget Embed secure card collection and tokenize for server-side payment Refresh the security code on a stored card before charging it again # Quickstart Source: https://docs.henrylabs.ai/v1/sdk/client/getting-started/quickstart Install the Henry JS SDK, embed a checkout widget, and handle events in under five minutes. This guide walks through the most common setup: embedding the Checkout Widget on a product page. If you're collecting card details for a custom payment flow, jump to the [Card Element guide](/v1/sdk/client/elements/card-element) instead. ## Prerequisites * A Widget ID from the [Henry Dashboard](https://app.henrylabs.ai/dashboard/settings/app/developer) * A product URL (any publicly accessible product page) *** ## Setup ```bash npm theme={null} npm install @henrylabs/js ``` ```bash bun theme={null} bun add @henrylabs/js ``` ```bash pnpm theme={null} pnpm add @henrylabs/js ``` ```bash yarn theme={null} yarn add @henrylabs/js ``` Or load via CDN - no build step required: ```html theme={null} ``` When loaded via CDN, use `window.Henry` instead of the ES module import. ```html theme={null}
```
```js theme={null} import Henry from "@henrylabs/js"; const el = Henry.createCheckoutElement("#checkout-container", { widgetId: "widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", items: [ { link: "https://www.nike.com/t/some-product/ABC-123", quantity: 1, selectedOptions: ["White", "10"], }, ], }); ```
*** ## End-to-end walkthrough ### Step 1 - Mount the widget `createCheckoutElement` takes a CSS selector (or DOM element) and an options object. Henry renders the widget inside your container immediately. ```js theme={null} import Henry from '@henrylabs/js'; const el = Henry.createCheckoutElement('#checkout-container', { widgetId: 'widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', items: [ { link: 'https://www.nike.com/t/air-max-270/AH8050-002', quantity: 1, selectedOptions: ['Black', '10'], }, ], display: { size: 'compact', // "full" | "comfortable" | "compact" | "bubble" | "direct" theme: 'system', // "light" | "dark" | "system" }, }); ``` See [Checkout Widget](/v1/sdk/client/widgets/checkout) for the full list of display sizes and what each looks like. *** ### Step 2 - Listen for checkout events Use `.on()` to respond when the buyer completes or closes checkout: ```js theme={null} el.on('checkout-complete', ({ status, order }) => { console.log('Checkout finished:', status); // show a confirmation UI, redirect, etc. }); el.on('checkout-closed', ({ source }) => { // source is "user" (buyer closed) or "system" (programmatic close) console.log('Checkout closed by:', source); }); ``` *** ### Step 3 - Clean up Call `.destroy()` when the widget is no longer needed to unmount it and remove all listeners: ```js theme={null} el.destroy(); ``` *** ### React Use a `ref` guard to prevent double-mounting in React Strict Mode: ```tsx theme={null} import { useEffect, useRef } from 'react'; import Henry from '@henrylabs/js'; function ProductPage() { const initialized = useRef(false); useEffect(() => { if (initialized.current) return; initialized.current = true; const el = Henry.createCheckoutElement('#checkout-container', { widgetId: 'widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', items: [{ link: 'https://www.nike.com/t/air-max-270/AH8050-002', quantity: 1 }], display: { size: 'compact', theme: 'dark' }, }); el.on('checkout-complete', ({ status, order }) => { console.log('Done:', status, order); }); return () => el.destroy(); }, []); return
; } ``` *** ## Next steps All display modes, settings, and events for the checkout widget Embed a secure card input for custom payment flows # Checkout Widget Source: https://docs.henrylabs.ai/v1/sdk/client/widgets/checkout Embed a fully managed checkout button and modal for one or more products. The Checkout Widget renders a buy-now button directly in your page. When clicked, it opens Henry's hosted checkout modal where the buyer completes payment, address, and order confirmation. Henry handles everything - you supply the product links and listen for events. *** ## Usage ```js theme={null} import Henry from '@henrylabs/js'; const el = Henry.createCheckoutElement('#checkout-container', { widgetId: 'widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', items: [ { link: 'https://www.nike.com/t/air-max-270/AH8050-002', quantity: 1, selectedOptions: ['Black', '10'], }, ], }); ``` Add a container in your HTML: ```html theme={null}
``` *** ## Options ### `widgetId` `string` `*required` Your Widget ID from the [Henry Dashboard](https://app.henrylabs.ai/dashboard/settings/app/developer). *** ### `items` `CartItem[]` `*required` One or more products to include in the checkout. Each item requires a `link` (a direct product page URL) and optionally accepts: | Field | Type | Description | | - | - | - | | `link` | `string` | Direct product URL | | `quantity` | `number` | Number of units. Defaults to `1` | | `selectedOptions` | `string[]` | One value per product option, in order, e.g. `['Black', '10']`. Spell each value as the store does, or a different variant can be bought | | `selectedShipping` | `{ id?: string; value?: string }` | Pre-select a shipping option | | `coupons` | `string[]` | Coupon codes to apply | | `metadata` | `Record` | Arbitrary metadata passed through to orders | ```js theme={null} items: [ { link: 'https://www.nike.com/t/air-max-270/AH8050-002', quantity: 2, selectedOptions: ['Black', '10'], coupons: ['SAVE10'], metadata: { source: 'product-page' }, }, ]; ``` *** ### `display` `object` Controls how the widget is rendered. #### `display.size` | Value | Description | | - | - | | `"full"` | Full-width button | | `"comfortable"` | Standard-sized button with logo and label | | `"compact"` | Smaller button, good for tight layouts | | `"bubble"` | Floating circular button | | `"direct"` | Opens the checkout iframe directly without a button trigger | ```js theme={null} display: { size: 'compact'; } ``` #### `display.theme` | Value | Description | | - | - | | `"light"` | Light color scheme | | `"dark"` | Dark color scheme | | `"system"` | Follows the user's OS preference | ```js theme={null} display: { theme: 'system'; } ``` *** ### `settings` `CartSettings` Optional settings that control checkout behavior, service fees, and event triggers. #### `settings.options` | Field | Type | Description | | - | - | - | | `allowPartialPurchase` | `boolean` | Whether the buyer can remove items and check out with a subset | | `collectBuyerEmail` | `"off" \| "required" \| "optional"` | Controls email collection | | `collectBuyerAddress` | `"off" \| "required" \| "optional"` | Controls address collection | | `collectBuyerPhone` | `"off" \| "required" \| "optional"` | **Deprecated, ignored.** Phone is always required | ```js theme={null} settings: { options: { allowPartialPurchase: true, collectBuyerEmail: "required", collectBuyerAddress: "optional", }, } ``` #### `settings.serviceFeeFixed` Add a fixed service fee to every checkout. ```js theme={null} settings: { serviceFeeFixed: { value: 2.99, currency: "USD" }, } ``` #### `settings.serviceFeePercent` Add a percentage-based service fee (0–100). ```js theme={null} settings: { serviceFeePercent: 5, } ``` #### `settings.events` Fire webhooks or send emails on order state changes. ```js theme={null} settings: { events: [ { type: "order.purchase.complete", data: [{ type: "send_webhook", webhookUUID: "..." }], }, ], } ``` *** ## Events Use `.on()` to subscribe to checkout events and `.off()` to remove a listener. ### `checkout-complete` Fires when the buyer successfully completes checkout. ```js theme={null} el.on('checkout-complete', ({ status, order }) => { console.log('Status:', status); console.log('Order ID:', order?.id); }); ``` | Field | Type | Description | | - | - | - | | `status` | `string` | Final order status | | `order` | `Order \| undefined` | Order object if available | ### `checkout-closed` Fires when the checkout modal closes, either by the buyer or programmatically. ```js theme={null} el.on('checkout-closed', ({ source }) => { console.log('Closed by:', source); // "user" | "system" }); ``` *** ## Methods ### `.on(event, callback)` → `this` Subscribe to a checkout event. Returns the element instance for chaining. ```js theme={null} el.on('checkout-complete', handler); ``` ### `.off(event, callback)` → `this` Remove a previously registered listener. ```js theme={null} el.off('checkout-complete', handler); ``` ### `.destroy()` Unmount the widget and clean up all event listeners. Call this when navigating away or unmounting a component. ```js theme={null} el.destroy(); ``` *** ## TypeScript ```ts theme={null} import type { CartItem, CheckoutOptions } from '@henrylabs/js'; const options: CheckoutOptions = { widgetId: 'widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', items: [{ link: 'https://www.nike.com/t/air-max-270/AH8050-002', quantity: 1 }], display: { size: 'compact', theme: 'dark' }, }; ``` *** ## React Use a `ref` guard to prevent double-mounting in Strict Mode: ```tsx theme={null} import { useEffect, useRef } from 'react'; import Henry from '@henrylabs/js'; function BuyNow({ productUrl }: { productUrl: string }) { const initialized = useRef(false); useEffect(() => { if (initialized.current) return; initialized.current = true; const el = Henry.createCheckoutElement('#checkout-container', { widgetId: 'widget_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', items: [{ link: productUrl, quantity: 1 }], display: { size: 'comfortable', theme: 'system' }, }); el.on('checkout-complete', ({ status, order }) => { console.log('Order placed:', order?.id, 'Status:', status); }); return () => el.destroy(); }, [productUrl]); return
; } ``` *** ## Related Embed a secure card input for custom payment flows Build headless checkout flows server-side # Agent Skills Source: https://docs.henrylabs.ai/v1/sdk/server/agents/skills Install Henry's skills in Claude Code, Cursor, Codex, and other coding agents. ## Overview Henry ships an open-source skills bundle, [Henry-Social/skills](https://github.com/Henry-Social/skills), that teaches AI coding agents (Claude Code, Cursor, Codex, and 30+ others) to build with — and shop through — the Henry API. Install it once and your agent knows how to integrate Henry into the app it's building, without pasting docs into the prompt. The bundle contains two skills: * **`integrate`** — fires when you ask the agent to add commerce to your app. It teaches [`@henrylabs/sdk`](/v1/sdk/server/getting-started/quickstart) setup, product search, universal carts, hosted vs. headless checkout, async-job polling, webhooks, and go-live steps. * **`shop`** — a live demo skill: ask for a product and the agent searches real merchants, builds a cart, and hands back a hosted checkout link. ## Prerequisites * A Henry API key from the [Henry Dashboard](https://app.henrylabs.ai) — start with a sandbox key * `HENRY_SDK_API_KEY` exported in the shell that launches your agent (the plugin sends it to the hosted MCP server): ```sh theme={null} export HENRY_SDK_API_KEY="your_api_key" ``` ## Install Install the full plugin — the two skills plus the [Henry MCP server](/v1/sdk/server/mcp/remote-mcp), wired up automatically. Run inside a Claude Code session: ``` /plugin marketplace add Henry-Social/skills /plugin install henry@henrylabs ``` The skills are namespaced under the plugin: `/henry:integrate` and `/henry:shop`. Updates ship automatically whenever the plugin repo changes. Any agent that supports the [Agent Skills standard](https://skills.sh/Henry-Social/skills) can install the bare skills from your terminal: ```sh theme={null} npx skills add Henry-Social/skills ``` This copies the skills into your project. It does not configure the Henry MCP server — add it separately if you want live commerce tools (see [MCP Server](/v1/sdk/server/mcp/remote-mcp) for per-client setup). ## Use it Paste a prompt like one of these into your agent: * "Help me integrate Henry into my project using the integrate skill." * "Add cross-merchant product search and checkout to my app with Henry." * In Claude Code, try the demo directly: `/henry:shop white sneakers under $100` Invoke the `integrate` skill whenever you're wiring Henry features — product discovery, carts, checkout, order tracking — into a backend or full-stack app. Building with an in-browser tool like Lovable or v0? Install the skill in a terminal agent alongside it: the terminal agent handles the Henry API integration while the browser tool manages the UI. ## Related * [MCP Server](/v1/sdk/server/mcp/remote-mcp) — the live commerce tools the Claude Code plugin bundles; connect it to any MCP-compatible client. * [Quickstart](/v1/sdk/server/getting-started/quickstart) — the SDK flow the `integrate` skill teaches your agent. # Core Concepts Source: https://docs.henrylabs.ai/v1/sdk/server/getting-started/core-concepts Understand async jobs, cart lifecycle, checkout modes, and order states before you build ## The shopping flow Henry's features are **modular** - use just the ones you need. You can call `cart.checkout.purchase` with a product link directly, use `products.details` without ever touching the cart, or run the full pipeline end-to-end: ```mermaid theme={null} flowchart TD A["🔍 Product Discovery
search · details"] --> B["🛒 Universal Cart
create · add · update · remove"] B --> C{Checkout mode?} C -->|Hosted| D["🌐 Hosted Checkout
'checkoutUrl' → Henry modal"] C -->|Headless| E["⚡ Headless Checkout
'cart.checkout.purchase'
full UI control"] D --> F["📦 Order
pending → processing → complete"] E --> F ``` *** ## Async jobs Product search, product details, checkout details, and `cart.checkout.purchase` are **async operations**. When you call them, Henry immediately returns a `refId` and a `status`. You have three options for handling async results: 1. **Sync mode** - pass `mode: "sync"` and the request blocks for up to 30 seconds until the result is ready. Simplest approach for fast operations. 2. **Poll** - use the corresponding `poll*` method until `status` becomes `complete` (or `failed`). 3. **Webhooks** - have Henry push the result to your server when it's ready. **Statuses:** | Status | Meaning | | - | - | | `pending` | Queued, not started yet | | `processing` | Actively running | | `complete` | Results are ready in `result` | | `failed` | Hit an unrecoverable error - check `error` | See [Polling](/v1/sdk/server/guides/polling) for a reusable helper and retry strategies, or [Webhooks](/v1/sdk/server/guides/webhooks) to have Henry send results to you instead. *** ## Cart lifecycle Create a cart with one or more product links. From there you can add, update, or remove items freely. When ready, send the buyer to checkout. Items are identified by their product URL - no separate product ID needed. `cart.create` returns a `checkoutUrl` immediately, so there's no extra step to set up hosted checkout. *** ## Checkout modes Henry collects shipping, payment, and taxes via a hosted modal. Submit payment and shipping directly from your server. Full UI control. *** ## Order states After checkout (either mode), Henry creates an order and processes it asynchronously across one or more merchants. ```mermaid theme={null} stateDiagram-v2 [*] --> pending : Order created pending --> processing : Payment accepted processing --> complete : All items placed processing --> partially_complete : Some items placed processing --> failed : No items placed pending --> cancelled : Order cancelled processing --> cancelled : Order cancelled complete --> [*] partially_complete --> [*] failed --> [*] cancelled --> [*] ``` | Status | Description | | - | - | | `pending` | Order received, payment not yet confirmed | | `processing` | Payment confirmed, placing items with merchants | | `complete` | All items successfully placed - `result.costs` is populated | | `partially_complete` | Some items placed, the rest failed - check each `products[].status` | | `failed` | No items were placed - `error` has details | | `cancelled` | The order was cancelled | Use `orders.list({ cartId })` to fetch orders associated with a specific cart, or omit `cartId` to list all orders for your app. *** ## Merchants Every product `link` is associated with a merchant `host` (e.g. `nike.com`). You can browse supported merchants with `merchants.list` - useful for building merchant-selection filters before adding them to a cart. *** ## Monetization Every completed order generates a service fee for your application. Henry handles payout calculations, merchant relationships, and reporting. You configure service fee amounts (fixed + percent) in the `settings` object when creating a cart. ```typescript theme={null} await henry.cart.create({ items: [...], settings: { serviceFeePercent: 5, // 5% of order total serviceFeeFixed: { value: 1.99, currency: 'USD' }, // + $1.99 flat }, }); ``` # Introduction Source: https://docs.henrylabs.ai/v1/sdk/server/getting-started/introduction Build embedded shopping experiences - product discovery, universal cart, and order management - with a single SDK The **Henry SDK** gives your application a full commerce stack out of the box. Search any merchant's catalog, build multi-merchant carts, launch checkout in seconds, and track orders - all through a type-safe TypeScript SDK backed by a single API key. Search any merchant's catalog and fetch rich product details with variants, pricing, and images Build carts with items from any merchant - create, add, update, remove, and fetch in one place Purchase your carts via headless checkout or the hosted frame Track and manage every order through to completion and beyond *** ## Merchant coverage Henry runs against each merchant's live storefront, so there is no per-merchant integration for you to build. Coverage is tracked per capability: a merchant can support **product discovery** (search and product details) and, on top of that, **checkout**. ### Product discovery Search the catalog and fetch full product details - variants, pricing, images - across 1,300+ merchants, including: ### Checkout Place orders end to end - cart, shipping, payment, and order confirmation - at merchants like: Coverage grows every week as new merchants come online. Call `merchants.list` or `merchants.retrieve` for the live list and each merchant's per-method `coverage` - see the [Merchants guide](/v1/sdk/server/guides/merchant). *** ## Get started in minutes Go to the [Henry Dashboard](https://app.henrylabs.ai) and create an app to get your API key. ```bash npm theme={null} npm i @henrylabs/sdk ``` ```bash bun theme={null} bun add @henrylabs/sdk ``` ```bash pnpm theme={null} pnpm add @henrylabs/sdk ``` ```bash yarn theme={null} yarn add @henrylabs/sdk ``` ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` Always initialize server-side. Your API key must never be exposed in browser or mobile code. ```typescript theme={null} const search = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max' } }); // poll until results are ready let result = search; while (result.status === 'pending' || result.status === 'processing') { await new Promise(r => setTimeout(r, 1000)); result = await henry.products.pollSearch({ refId: search.refId }); } console.log(result.result?.products[0]); ``` *** ## Explore the guides End-to-end walkthrough - search → cart → checkout → order in one file Understand async jobs, cart lifecycle, and checkout modes Full endpoint reference with live playground # Quickstart Source: https://docs.henrylabs.ai/v1/sdk/server/getting-started/quickstart Search products, build a cart, launch checkout, and track an order. Want to skip the reading and run it live? Check out our [demo app](https://demo.henrylabs.ai). This guide shows the full E2E flow - search → cart → checkout → order tracking. You don't have to use all of it. Jump to whichever feature you need: you can call `cart.checkout.purchase` directly with a product link, or use `products.details` standalone without ever touching the cart. ## Prerequisites * Node.js 20+ (or Bun/Deno) * An API key from the [Henry Dashboard](https://app.henrylabs.ai) * A server-side environment (API key must stay private) ## Setup ```bash npm theme={null} npm i @henrylabs/sdk ``` ```bash bun theme={null} bun add @henrylabs/sdk ``` ```bash pnpm theme={null} pnpm add @henrylabs/sdk ``` ```bash yarn theme={null} yarn add @henrylabs/sdk ``` ```bash theme={null} #.env.local HENRY_SDK_API_KEY=your_api_key_here ``` ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` *** ## End-to-end walkthrough ### Step 1 - Search for products Product search is async. The initial call returns a `refId` so in this example we'll poll until results arrive. ```typescript theme={null} // Kick off the search const search = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max' }, limit: 10, }); // Poll until complete async function pollUntilDone(initial: T, poll: (args: { refId: string }) => Promise, intervalMs = 1000): Promise { let current = initial; while (current.status === 'pending' || current.status === 'processing') { await new Promise((r) => setTimeout(r, intervalMs)); current = await poll({ refId: initial.refId }); } return current; } const searchResult = await pollUntilDone(search, (args) => henry.products.pollSearch(args)); const products = searchResult.result?.products ?? []; const first = products[0]; console.log(first.name, first.price?.value, first.price?.currency); // → "Nike Air Max 270" "150.00" "USD" ``` Cache the `refId` - you can re-poll any time to retrieve results without re-running the search. *** ### Step 2 - Fetch product details Use the product `link` from the search result to pull full details: rich images, all options, and live availability. ```typescript theme={null} const detailResult = await henry.products.details({ link: first.link, }); const detail = await pollUntilDone(detailResult, (args) => henry.products.pollDetails(args)); const product = detail.result; console.log(product?.name, product?.options); ``` *** ### Step 3 - Create a cart Pass one or more product links to `cart.create`. The response includes a `cartId` and a ready-to-use `checkoutUrl` for hosted checkout. ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: first.link, quantity: 1, selectedOptions: ['regular', 'black', '10-w'], }, ], }); const { cartId, checkoutUrl } = cart.data; console.log('Cart:', cartId); console.log('Checkout URL:', checkoutUrl); // → https://checkout.henrylabs.ai/cart/ ``` *** ### Step 4 - Add more items (optional) You can add products to an existing cart at any time. ```typescript theme={null} await henry.cart.item.add(cartId, { item: { link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', quantity: 2, }, }); ``` *** ### Step 5 - Launch hosted checkout The `checkoutUrl` returned by `cart.create` points to Henry's hosted checkout modal. Redirect your user, open it in an iframe, or pop it in a modal - Henry handles payment, address, and tax collection. ```typescript theme={null} // Option A: Full page redirect (server-side) res.redirect(checkoutUrl); // Option B: Embed in an iframe (client-side) const iframe = document.createElement('iframe'); iframe.src = checkoutUrl; iframe.style.cssText = 'width:100%;height:700px;border:none;border-radius:12px'; document.getElementById('checkout-container')!.appendChild(iframe); // - [Optional]: Listen for completion events window.addEventListener('message', (event) => { if (!event.origin.endsWith('.henrylabs.ai')) return; if (event.data?.event === 'checkout-complete') { console.log('Order placed:', event.data.data.order.id); } }); ``` Need full control over the checkout UI? See [Headless Checkout](/v1/sdk/server/guides/checkout#headless-checkout) *** ### Step 6 - Track the order The recommended approach is webhooks - Henry will POST to your endpoint when the order status changes, so you don't have to poll. Register a webhook endpoint in the [Henry Dashboard](https://app.henrylabs.ai), then handle the events: ```typescript theme={null} app.post('/webhooks/henry', express.raw({ type: 'application/json' }), async (req, res) => { if (!verifyHenryWebhook(req.body.toString(), req.headers['x-henry-signature'], req.headers['x-henry-timestamp'])) { return res.sendStatus(401); } const { event, data } = JSON.parse(req.body.toString()); if (event === 'order.purchase.complete' || event === 'order.purchase.partially_complete') { // Placed — every item, or some (check data.items[].status) console.log('Order placed:', data.orderId, data.results?.costs); } else if (event === 'order.purchase.failed') { console.log('Order failed:', data.orderId); } res.sendStatus(200); }); ``` See the [Webhooks guide](/v1/sdk/server/guides/webhooks) for signature verification and the full list of events. If you need to check order status on demand, use `orders.list({cartId})`. *** ## Next steps Deep dive: search filters, async polling, and rich product details Create, manage, and configure multi-merchant carts Hosted checkout with postMessage events, and headless checkout List, filter, and poll orders across all your users # Checkout Source: https://docs.henrylabs.ai/v1/sdk/server/guides/checkout Convert a cart into a paid order - via hosted checkout or headless purchase ## Two paths to checkout Henry supports two checkout modes. Pick the one that fits your needs: The `checkoutUrl` returned by `cart.create` is a fully hosted checkout page. Redirect, iframe, or popup. Henry collects shipping, payment, and tax. Call `checkout.purchase` from your server with card token and shipping info. Henry processes payment and places the order with the merchant. You own the UI completely. *** # Hosted Checkout ### Overview Henry's Hosted Checkout is the fastest way to add commerce capabilities to your application. With our hosted checkout solution, you can accept payments without handling sensitive payment data or building complex UI components. Simply embed our checkout experience via iframe, webview, or redirect. ### Why Choose Hosted Checkout? Pre-built UI components and hosted pages get you live fast We handle all payment security and compliance requirements Responsive design works seamlessly across all devices **Perfect for**: Startups and businesses that want to move fast and launch quickly. ### How It Works Hosted Checkout uses a simple redirect or embedded iframe approach: ```mermaid theme={null} sequenceDiagram participant User participant YourApp participant Henry participant Merchant User->>YourApp: Select products YourApp->>Henry: Create checkout session Henry-->>YourApp: Return checkout URL YourApp->>User: Redirect/Show iframe User->>Henry: Complete payment Henry->>Merchant: Place order Henry->>YourApp: Order confirmation YourApp->>User: Show confirmation ``` ### Embed options The simplest integration - just redirect the user to the checkout URL. ```typescript theme={null} // Server-side (e.g. Next.js API route) const cart = await henry.cart.create({ items }); res.redirect(cart.data.checkoutUrl); // Client-side window.location.href = checkoutUrl; ``` Keep users on your site with an embedded checkout experience. ```html theme={null}
``` ```typescript theme={null} const cart = await henry.cart.create({ items }); document.getElementById('henry-checkout').src = cart.data.checkoutUrl; // Listen for completion window.addEventListener('message', event => { if (!event.origin.endsWith('.henrylabs.ai')) return; if (event.data?.event !== 'checkout-complete') return; const { order } = event.data.data; console.log('Order placed:', order.id, order.status); document.getElementById('henry-checkout').style.display = 'none'; showOrderConfirmation(order.id); }); ```
Balance between full-page and inline with a popup window. ```typescript theme={null} const cart = await henry.cart.create({ items }); const modal = window.open( cart.data.checkoutUrl, 'henry-checkout', 'width=520,height=750,left=200,top=100', ); window.addEventListener('message', event => { if (!event.origin.endsWith('.henrylabs.ai')) return; if (event.data?.event === 'checkout-complete') { modal?.close(); handleSuccess(event.data.data.order); } }); ``` Use `react-native-webview` to embed Henry checkout in a mobile app. ```tsx theme={null} import { WebView } from 'react-native-webview'; function CheckoutScreen({ checkoutUrl }: { checkoutUrl: string }) { return ( { const message = JSON.parse(event.nativeEvent.data); if (message.event === 'checkout-complete') { navigation.navigate('OrderConfirmation', { orderId: message.data.order.id, }); } }} /> ); } ```
Want to match your color scheme? Append `?theme=` to the checkout URL. *** # Headless Checkout ### Overview Headless Checkout gives you full control over the buyer experience. You collect shipping and payment info in your own UI, then call Henry's API server-side to process payment and place the order. To securely collect card details in your UI, use the [Card Element](/v1/sdk/client/elements/card-element). It tokenizes the card and returns a `cardToken` to pass to `cart.checkout.purchase`. Charging a **returning buyer** with a stored `cardToken` needs one extra step. A security code can't be retained, so it expires while the card number and expiry stay valid — re-collect just the code with the [CVC Element](/v1/sdk/client/elements/cvc-element) (or `POST /card/{cardToken}/cvv` server-side) and keep using the same `cardToken`. You never need to re-collect the full card. If you skip this, `cart.checkout.purchase` fails fast with a `400` once the stored code has lapsed — re-collect the code and retry with the same `cardToken`. **Keep card tokenization on the client to stay out of PCI scope.** The Card Element collects and tokenizes card data inside an isolated iframe, so raw card numbers never touch your servers. A server-side `henry.card.tokenize()` (`POST /card/tokenize`) also exists, but sending the raw PAN, CVV, and expiry through your own backend brings your systems into PCI DSS scope (typically SAQ D). Only reach for it if you already operate a PCI-compliant environment. ### Why Choose Headless Checkout? Own every pixel of the checkout experience - no iframes, no redirects Seamlessly match your existing design system and user flows All payment processing happens on your server - no client-side exposure Headless checkout is enabled for every team by default - no extra setup beyond an API key. Purchases are billed as `checkout_purchase` credits, exactly like hosted checkout. ### How It Works ```mermaid theme={null} sequenceDiagram participant App as Your App (server) participant Henry as Henry API App->>Henry: cart.create({ items }) Henry-->>App: { cartId, checkoutUrl } Note over App: Collect buyer info in your own UI App->>Henry: cart.checkout.purchase(cartId, { buyer }) Henry-->>App: { refId, status: "pending" } loop Poll ~2s App->>Henry: cart.checkout.pollPurchase({ refId }) Henry-->>App: { status: "processing" } end Henry-->>App: { status: "complete", result: { costs } } ``` ### Preview costs before charging To show the buyer a full cost breakdown before placing the order, call `cart.checkout.details` with the shipping address. It returns the merchant's shipping options plus `costs` — `subtotal`, estimated `tax`, `shipping`, and `total`. ```typescript theme={null} const details = await client.cart.checkout.details(cartId, { buyer: { shippingAddress: { line1: '123 Main St', city: 'Anytown', province: 'CA', postalCode: '12345', countryCode: 'US', }, }, mode: 'sync', }); const { costs, shippingOptions } = details.data.jobs[0]!.result!.data!; ``` `costs.tax` is a destination-based estimate — the same number the hosted checkout shows as "Estimated Taxes". The merchant's exact tax is only final at their own checkout. A just-added product whose price is still resolving contributes no tax (and a `$0` subtotal) until its price lands — retry in about a minute, or fetch product details for the items first. Call `cart.checkout.purchase` from your server with the buyer's contact, shipping and card info. `buyer.phone` is required and must be a valid phone number — E.164 (`+19175551234`) is preferred; a national number is read against the shipping address country. A missing or invalid phone returns `400`. ```typescript theme={null} const purchase = await henry.cart.checkout.purchase('crt_sa2aEsCz9PRM', { buyer: { name: { firstName: 'Jane', lastName: 'Doe' }, email: 'jane@example.com', phone: '+19175551234', shippingAddress: { line1: '350 5th Ave', line2: 'Floor 21', city: 'New York', province: 'NY', postalCode: '10118', countryCode: 'US', }, card: { nameOnCard: { firstName: 'Jane', lastName: 'Doe' }, details: { cardToken: '...', }, // billingAddress optional - defaults to shippingAddress if omitted }, }, mode: 'async', // or 'sync' to wait up to 30s for completion }); console.log(purchase.refId, purchase.status); // → "ckp-ref_38lb3..." "pending" ``` Poll every \~2 seconds until the order reaches a terminal state. ```typescript theme={null} let order = purchase; while (order.status === 'pending' || order.status === 'processing') { await new Promise((r) => setTimeout(r, 2000)); order = await henry.cart.checkout.pollPurchase({ refId: purchase.refId }); } if (order.status === 'complete' || order.status === 'partially_complete') { // Purchased — every item, or some. Check products[].status for which. const { subtotal, serviceFee, total } = order.result!.costs; console.log(`Order total: ${total.value} ${total.currency}`); console.log('Items:', order.products); } else { // "failed" (nothing was purchased) or "cancelled" console.error('Order not placed:', order.error); } ``` Alternatively, skip polling entirely by attaching a webhook when you create the cart - Henry will fire it when the order completes. ```typescript theme={null} const cart = await henry.cart.create({ items: [...], settings: { events: [ { type: 'order.purchase.complete', data: [ { type: 'send_webhook', webhookUUID: 'your-webhook-uuid', }, ], }, ], }, }); ``` Webhooks are registered in the [Henry Dashboard](https://app.henrylabs.ai). See [Universal Cart - Cart events](/v1/sdk/server/guides/universal-cart#cart-events) for the full event and action type reference. Pass `overrideProducts` to adjust quantities at purchase time without modifying the cart. ```typescript theme={null} await henry.cart.checkout.purchase(cartId, { buyer: { /* ... */ }, overrideProducts: { 'https://www.nike.com/t/air-max-270-shoes/AH8050-002': 2, // bump to 2 'https://www.adidas.com/us/ultraboost-22/GX5591.html': null, // exclude }, }); ``` **Beta:** if your app uses [issued cards](/v1/sdk/server/guides/issued-cards) (email [support@henrylabs.ai](mailto:support@henrylabs.ai) for access), a headless purchase also takes `checkoutDetailsRefIds` from the details call above: Henry Labs pays the merchant with a virtual card funded by your buyer's card. ## Next steps List and track orders after checkout - filter by status, cartId, and more Learn how to build and manage the cart before checkout # Issued Cards (Beta) Source: https://docs.henrylabs.ai/v1/sdk/server/guides/issued-cards Check out on a single-purpose virtual card, paid for by your buyer's card **Issued cards are in beta.** Everything on this page, including issued checkout and the `card.issue`, `card.retrieve`, `card.close` and `card.reveal` endpoints, is enabled per account and may change. To request access, email [support@henrylabs.ai](mailto:support@henrylabs.ai). Until access is enabled, these endpoints return `403`. ## What it is With issued cards, a checkout doesn't send your buyer's card to the merchant. Henry Labs charges your buyer's tokenized card (collect it with the [Card Element](/v1/sdk/client/elements/card-element)), issues a **virtual card** for that amount, and checks out on the virtual card instead. * **The buyer is charged once, by Henry Labs.** Their statement reads `HENRY LABS* `, for example `HENRY LABS* ZARA`. * **The buyer doesn't need to be present.** Henry saves the card for later charges, so no CVC is needed, even days later. A card from the Card Element is saved the first time it funds an issued card, so use it within an hour of collecting it. * **The merchant never sees your buyer's card.** Each issued card has a hard spending limit and locks to the merchant that charges it. * **Unspent money goes back automatically.** When the card settles, the buyer is refunded whatever it didn't spend, and later merchant refunds on returns are passed back too. There are two ways to use it: Recommended. Henry Labs sizes, charges and issues the card for you inside `cart.checkout.purchase`, from a checkout quote. Call `card.issue` with an amount you choose, then pay with the returned card token. **Get your buyer's permission for future charges.** Both flows charge the buyer's card without them present, which card networks treat as a merchant-initiated charge. When your buyer enters their card, tell them it may be charged later for their orders, and keep a record that they agreed. ## Issued checkout For apps using issued checkout, every headless purchase pays with an issued card. It takes one extra field: the checkout quote the card is sized from. Run checkout details for the buyer's shipping address. You get one `refId` per merchant in the cart. ```ts theme={null} const details = await henry.cart.checkout.details(cartId, { buyer: { shippingAddress }, mode: "sync", }); const refIds = details.data.jobs.map((job) => job.refId); const totals = details.data.jobs.map((job) => job.result?.data?.costs.total); ``` Show your buyer the total before you charge them. Pass every `refId` as `checkoutDetailsRefIds`, within **30 minutes** of the quote, with the same items and shipping address. ```ts theme={null} const purchase = await henry.cart.checkout.purchase(cartId, { buyer: { name: { firstName: "Ada", lastName: "Lovelace" }, email: "ada@example.com", phone: "+12125551234", shippingAddress, card: { details: { cardToken: buyerCardToken } }, }, checkoutDetailsRefIds: refIds, }); ``` Henry Labs charges `buyerCardToken`, issues the card and starts the checkout on it. Poll or use webhooks as for any purchase. ### What the buyer is charged Each merchant's **quoted total, plus padding**, rounded up to the whole dollar. Padding absorbs small changes between the quote and the merchant's final total, such as tax recalculated at the last step. The default is **\$5 per merchant**; email [support@henrylabs.ai](mailto:support@henrylabs.ai) to change it for your app. | Cart | Quote | Charged | Issued card | | - | - | - | - | | One merchant | \$54.12 | $60 ($59.12 rounded up) | Locked to that merchant | | Two merchants | $80.00 + $40.00 | $85 + $45 = **\$130**, in one charge | One card usable at both | Whatever the merchants don't take is refunded to the buyer when the card settles (see [Refunds](#refunds)). For a single-merchant cart you can set the amount yourself with `capCents`, as long as it covers the quoted total. ### When a quote is refused Nothing is charged in any of these cases. Fix the quote and retry. | Problem | Response | | - | - | | No `checkoutDetailsRefIds` | `400` | | A merchant in the cart has no quote, or has two | `400` | | A `refId` that doesn't exist or isn't your app's | `404` | | A quote that's still running, failed, or is over 30 minutes old | `409` | | Items, quantities or the shipping address changed since the quote | `409` | | The amount is over your per-card maximum or open-card limit | `402` | | The buyer's card was declined | `402`, with Stripe's `stripeCode` and `declineCode` | Retrying a purchase with the same `checkoutDetailsRefIds` is safe: it returns the same order and never charges twice. Issued checkout currently applies to headless purchases. Hosted checkout orders pay with the buyer's card directly, as before. ## Issue a card yourself Use `card.issue` when you want to choose the amount, or use the card outside a Henry cart. ```ts theme={null} const { data: card } = await henry.card.issue( { capCents: 3000, fundingCardToken: buyerCardToken, merchantHost: "zara.com", ttlDays: 7, }, { idempotencyKey: "order-1234" }, ); // card.cardToken → "card_live_…" ``` * `capCents` is a **lifetime** limit: the most the card can ever be charged in total. It's rounded up to the whole dollar ($30.40 becomes $31). Size it to cover tax and shipping. * The buyer is charged the rounded amount before the card exists. * Send an idempotency key. Retrying with the same key returns the same card and never charges twice. Use the token anywhere a tokenized card works. ```ts theme={null} await henry.cart.checkout.purchase(cartId, { buyer: { // …name, email, phone, shipping address… card: { details: { cardToken: card.cardToken } }, }, }); ``` Issued cards don't need CVC re-collection: their security code stays valid until the card expires. `card.retrieve(cardToken)` returns the card's limit, expiry, live state, and what has been charged to it. `card.close(cardToken)` once the purchase has fully completed. The card then settles within about 15 minutes. Closing is optional: a card you leave alone settles when it expires. ## Reveal the card number If you run the checkout yourself instead of through `cart.checkout.purchase`, `card.reveal` returns an issued card's full number, expiry and security code. ```ts theme={null} const { data } = await henry.card.reveal(card.cardToken); // data.cardNumber, data.expiryMonth, data.expiryYear, data.cvv ``` **Receiving a full card number puts your systems in PCI DSS scope** (typically SAQ D). Don't log, store or display it. If you can, pay with `cart.checkout.purchase` and the `cardToken` instead: that never exposes the number. * **Separate beta access.** Reveal is enabled on its own, separately from card issuance. Email [support@henrylabs.ai](mailto:support@henrylabs.ai) to request it; until then it returns `403`. * **Issued cards only.** Only cards issued to your app can be revealed. A tokenized buyer card never can (`404`). * **Open cards only.** A card that's closed or expired returns `409`, as does a card whose state couldn't be confirmed (safe to retry). * **Rate limited.** 30 reveals per minute per app (`429` above that). Every reveal is recorded with the app and card. ## Refunds A card **settles** when it's closed or expires. At settlement, the buyer is refunded the difference between what they were charged and what the merchant actually took, usually within 15 minutes. | Situation | Refunded | | - | - | | Charged $60, merchant took $54.12 | \$5.88 | | The checkout failed and the card was never charged | Everything | | The merchant hasn't captured yet (some charge when they ship) | Settlement waits for the capture | | The buyer returns an item later | The merchant's refund is passed to the buyer, for up to 120 days after settlement | | The buyer disputed the charge with their bank | Nothing, so they aren't paid twice; the dispute decides | Don't close a card the merchant may still charge. A merchant that charges at shipping can't capture on a closed card. If you're unsure, leave the card open and let it settle when it expires. ## Limits | Limit | What it means | | - | - | | Per-card maximum | The largest amount one card can be issued for | | Open-card limit | The total of all your cards that haven't settled yet | | Expiry | Cards expire after your app's default number of days unless you set `ttlDays` (up to 90) | | Currency | Cards are issued in US dollars | Henry Labs sets these for your app during the beta; email [support@henrylabs.ai](mailto:support@henrylabs.ai) to change them. A request over either limit returns `402` and charges nothing. # Merchants Source: https://docs.henrylabs.ai/v1/sdk/server/guides/merchant Browse, filter, and retrieve the merchants supported by Henry - with per-method coverage statuses ## Overview Every product `link` belongs to a merchant `host` (e.g. `nike.com`). The `merchants` resource lets you: * `merchants.list` - browse all merchants enabled for your application, with filters * `merchants.search` - free-text merchant search over name + host (typeahead-grade) * `merchants.retrieve` - fetch a single merchant by host Each merchant comes with its **per-method coverage statuses** (checkout, productDetails, productSearch) - so you can pick the right merchants for your catalog and gate features per capability. *** ## Prerequisites ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` *** ## List merchants Call `merchants.list` to retrieve all merchants available to your app. ```typescript theme={null} const { data: merchants } = await henry.merchants.list({ limit: 40, }); for (const merchant of merchants) { console.log(merchant.name, merchant.host, merchant.coverage.checkout); // → "Nike" "nike.com" "supported" } ``` *** ## Retrieve a single merchant When you already know the `host`, `merchants.retrieve` fetches that merchant directly - no filtering, no pagination. ```typescript theme={null} const { data: nike } = await henry.merchants.retrieve('nike.com'); console.log(nike.name, nike.coverage.checkout); // → "Nike" "supported" ``` Returns `404 Not Found` if the host isn't enabled for your app. *** ## Search merchants When all you have is what a user typed - a name fragment, a host fragment - `merchants.search` resolves it to ranked merchants. It partial-matches the query against merchant **name and host** (case- and accent-insensitive), ranking prefix matches first, and returns a single ranked page. ```typescript theme={null} const { data: matches } = await henry.merchants.search({ q: 'nike', limit: 5, }); console.log(matches.map(m => `${m.name} (${m.host})`)); // → ["Nike (nike.com)", ...] ``` Each result carries the full merchant payload - including per-method `coverage` - so you can scope a [product search](/v1/sdk/server/guides/product-discovery) to a result's `host` or gate checkout affordances without a second lookup. | Parameter | Type | Default | Description | | - | - | - | - | | `q` | `string` | - | Free-text query matched against name + host | | `limit` | `number` | `10` | Maximum results (1–50) | Search is built for typeaheads: results are one ranked page with no cursor. Narrow the query instead of paging. *** ## Discover search filters `merchants.retrieve` also returns `searchFilters` - the variant-option filter axes (option name → display values) that [merchant-scoped product search](/v1/sdk/server/guides/product-discovery) accepts for this merchant. ```typescript theme={null} const { data: merchant } = await henry.merchants.retrieve('nike.com'); console.log(merchant.searchFilters?.options); // → { size: ["XS", "S", "M", "L"], color: ["Black", "White"] } // Pass any discovered value straight into a merchant search: const search = await henry.products.search({ type: 'merchant', filters: { merchant: 'nike.com', options: { size: 'M', color: 'Black' }, }, }); ``` `searchFilters` is `null` until the merchant's catalog has been indexed - its summary refreshes with the weekly catalog sync. Matching is case-insensitive and best-effort: option filters are honored for merchants served from Henry's search index and ignored by live search methods. *** ## Filter merchants Look up a specific merchant by domain: ```typescript theme={null} const { data } = await henry.merchants.list({ host: 'nike.com', }); const nike = data[0]; console.log(nike.name, nike.description); console.log(nike.categories); // ["fashion_and_accessories", "sports_and_outdoor"] console.log(nike.website.urls); // [{ value: "https://nike.com", type: "landing" }] console.log(nike.logo.urls); // [{ value: "https://..." }] ``` If you already know the host, prefer [`merchants.retrieve`](#retrieve-a-single-merchant) - it's a direct lookup instead of a filtered list. Partial-match search on merchant display name: ```typescript theme={null} const { data } = await henry.merchants.list({ name: 'nike', // case-insensitive partial match }); ``` Restrict results to merchants Henry actively supports (vs. still being onboarded), per method: ```typescript theme={null} // Merchants where checkout purchases are supported const { data } = await henry.merchants.list({ checkoutCoverage: 'supported', limit: 100, }); // Filters combine: searchable AND checkout-supported const { data: searchable } = await henry.merchants.list({ productSearchCoverage: 'supported', checkoutCoverage: 'supported', limit: 100, }); ``` One filter per method: `checkoutCoverage`, `productDetailsCoverage`, `productSearchCoverage`. `coverageStatus` is a deprecated alias of `checkoutCoverage` and is ignored when `checkoutCoverage` is also provided. Find all merchants in one or more categories: ```typescript theme={null} const { data } = await henry.merchants.list({ categories: ['fashion_and_accessories', 'sports_and_outdoor'], limit: 100, }); console.log(data.map(m => m.name)); // → ["Nike", "Adidas", "New Balance", ...] ``` *** ## Coverage Every merchant record includes per-method coverage statuses. Use them to gate features per-merchant and pick the right merchants for each capability. ```typescript theme={null} const { data: nike } = await henry.merchants.retrieve('nike.com'); // Per-method coverage: "supported" | "testing" | "unsupported" nike.coverage.checkout; // checkout purchases nike.coverage.productDetails; // product detail lookups nike.coverage.productSearch; // merchant-scoped product search // Deprecated alias of coverage.checkout - always the same value nike.coverageStatus; ``` *** ## Affiliate rates When one of Henry's affiliate networks covers a merchant, the record includes an `affiliate` object with the highest commission rate our coverage pays there — as a percentage of order value (`6` means 6%). Use it to decide which merchants to promote. ```typescript theme={null} const { data: nike } = await henry.merchants.retrieve('nike.com'); nike.affiliate?.maxCommissionRate; // e.g. 6 — or undefined when not covered ``` The object is omitted for merchants without affiliate coverage (or with no rate on file), so treat it as optional. *** ## Filter reference (`merchants.list`) | Parameter | Type | Default | Description | | - | - | - | - | | `limit` | `number` | `20` | Results per page (1–100) | | `cursor` | `string` | - | Pagination cursor from previous response | | `host` | `string` | - | Exact domain match (e.g. `"nike.com"`) | | `name` | `string` | - | Partial name match (e.g. `"nike"`) | | `checkoutCoverage` | `string` | - | Checkout coverage: `supported`, `testing`, or `unsupported` | | `productDetailsCoverage` | `string` | - | Product-details coverage: `supported`, `testing`, or `unsupported` | | `productSearchCoverage` | `string` | - | Product-search coverage: `supported`, `testing`, or `unsupported` | | `coverageStatus` | `string` | - | **Deprecated.** Alias of `checkoutCoverage`; ignored when both are sent | | `categories` | `string[]` | - | Filter by one or more merchant categories | ### Available categories `automotive`, `baby_and_kids`, `ecommerce`, `electronics_and_gadgets`, `fashion_and_accessories`, `finance`, `food`, `health_and_beauty`, `home_and_living`, `lifestyle_and_entertainment`, `other`, `sports_and_outdoor`, `travel`, `education` *** ## Error handling | Error | Cause | | - | - | | `401 Unauthorized` | Invalid API key | | `400 Bad Request` | Invalid `limit` or malformed `categories` | | `404 Not Found` | `merchants.retrieve` called with an unknown / disabled host | | Empty `data` array | No merchants match the filters (`merchants.list`) | *** ## Next steps Search products from a specific merchant using the `merchant` filter Add product links from supported merchants to a cart # Order Management Source: https://docs.henrylabs.ai/v1/sdk/server/guides/order-management List, filter, and track orders - from pending payment to completed fulfillment ## Overview After a checkout completes (hosted or headless), Henry creates an order and orchestrates fulfillment across one or more merchants. Use `orders.list` to fetch, filter, and monitor those orders from your backend. *** ## Prerequisites ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` *** ## List orders `orders.list` returns all orders for your application, newest first. It supports filtering by status and cart. ```typescript theme={null} const { data: orders } = await henry.orders.list({ limit: 20, }); for (const order of orders) { console.log(order.refId, order.status); } ``` ### Filter by status An order is `pending` or `processing` while it's being purchased, then settles: | Status | Meaning | | - | - | | `complete` | Every item was purchased | | `partially_complete` | Some items were purchased; check each `products[].status` | | `failed` | No items were purchased; `error` says why | | `cancelled` | The order was cancelled | ```typescript theme={null} // Only orders where every item was purchased const { data: completed } = await henry.orders.list({ status: 'complete', limit: 50, }); ``` ### Filter by cart Look up the order(s) tied to a specific cart: ```typescript theme={null} const { data: orders } = await henry.orders.list({ cartId: 'crt_sa2aEsCz9PRM', }); const order = orders[0]; console.log(order.status); // "complete" console.log(order.result?.costs.total); // { value: 149.99, currency: "USD" } ``` *** ## Track order updates Orders move through states asynchronously. You have two options: * **Poll for status** - query `orders.list` (filtered by `cartId`) on an interval until the order reaches a terminal state. * **Receive a webhook** - use `settings.events` on `cart.create` to fire a webhook when the order completes, avoiding polling entirely. See [Universal Cart - Cart events](/v1/sdk/server/guides/universal-cart#cart-events). *** ## Order statuses explained | Status | What it means | Terminal? | | - | - | - | | `pending` | Payment not yet confirmed | No | | `processing` | Payment accepted, placing items with merchants — or an item failed and the order has not settled yet (up to an hour); that item reads `processing` too, and `result` is withheld | No | | `complete` | Every item was purchased. `result.costs` is populated | ✅ | | `partially_complete` | Some items were purchased, the rest failed — check each `products[].status`. `result.costs` covers the purchased items | ✅ | | `failed` | No items were purchased — `error` has details | ✅ | | `cancelled` | The order was cancelled at any stage — `error` has details | ✅ | *** ## Filter reference | Parameter | Type | Default | Description | | - | - | - | - | | `limit` | `number` | `20` | Results per page (1–100) | | `cursor` | `string` | - | Pagination cursor from previous response | | `status` | `"pending" \| "processing" \| "complete" \| "partially_complete" \| "failed" \| "cancelled"` | - | Filter by order status | | `cartId` | `string` | - | Return only orders from this cart | *** ## Error handling | Error | Cause | | - | - | | `401 Unauthorized` | Invalid API key | | `400 Bad Request` | Invalid `cartId` format or out-of-range `limit` | | Empty `data` array | No orders found matching the filters | | `status: "cancelled"` | Check `order.error` for the failure reason | *** ## Next steps Review both hosted and headless checkout flows Understand cart creation and item management # Polling Source: https://docs.henrylabs.ai/v1/sdk/server/guides/polling Handle async operations with the polling pattern - product search, product details, and headless checkout ## Overview Several Henry operations are **async** - they return immediately with a `refId` and a `status`, and results arrive once processing completes. You poll the corresponding `poll*` endpoint until `status` reaches a terminal state. Operations that follow this pattern: | Operation | Trigger | Poller | | - | - | - | | Product search | `products.search` | `products.pollSearch` | | Product details | `products.details` | `products.pollDetails` | | Headless checkout | `cart.checkout.purchase` | `cart.checkout.pollPurchase` | | Checkout details | `cart.checkout.details` | `cart.checkout.pollDetails` | *** ## Statuses ```mermaid theme={null} stateDiagram-v2 direction LR [*] --> pending : Request received pending --> processing : Processing started processing --> complete : Results ready processing --> failed : Unrecoverable error complete --> [*] failed --> [*] ``` | Status | Terminal | Action | | - | - | - | | `pending` | No | Keep polling | | `processing` | No | Keep polling | | `complete` | ✅ | Read `result` | | `failed` | ✅ | Log `error`, investigate and retry | A purchase (`pollPurchase`) settles as `complete` (every item bought), `partially_complete` (some items bought), `failed` (nothing bought) or `cancelled`, and can stay `processing` for up to an hour after an item fails — that item reads `processing` too until the order settles. For purchases, prefer [webhooks](/v1/sdk/server/guides/webhooks) over long polling. *** ## Sync mode - skip polling entirely All async operations support `mode: "sync"`, which blocks the request for up to 30 seconds until the operation completes. If the result is ready within that window, you get it in a single call with no polling needed. ```typescript theme={null} const search = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max' }, mode: 'sync', }); // If completed within 30s, result is already populated if (search.status === 'complete') { console.log(search.result?.products); } ``` If the operation doesn't complete within 30 seconds, sync mode returns with a non-terminal status (`pending` or `processing`) and you'll need to fall back to polling. For operations that reliably take longer (e.g. headless checkout), prefer `mode: "async"` with the poll helper below. *** ## Reusable poll helper This generic helper works for any Henry async operation. Drop it in a shared utility file and import it wherever you need it. ```typescript theme={null} // utils/henry-poll.ts export async function pollJob( initial: T, poller: (args: { refId: string }) => Promise, options: { intervalMs?: number; maxAttempts?: number; } = {}, ): Promise { const { intervalMs = 1000, maxAttempts = 60 } = options; let result = initial; let attempts = 0; while ((result.status === 'pending' || result.status === 'processing') && attempts < maxAttempts) { await new Promise((r) => setTimeout(r, intervalMs)); result = await poller({ refId: initial.refId }); attempts++; } if (result.status === 'pending' || result.status === 'processing') { throw new Error(`Henry request ${initial.refId} timed out after ${maxAttempts} attempts`); } return result; } ``` *** ## Examples ### Product search ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; import { pollJob } from './utils/henry-poll'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY! }); const searchResult = await pollJob( await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max', sortBy: 'price-low-to-high', }, limit: 20, }), (args) => henry.products.pollSearch(args), ); if (searchResult.status !== 'complete') { throw new Error(`Search ${searchResult.status}: ${JSON.stringify(searchResult.error)}`); } const { products, pagination } = searchResult.result!; console.log(`Found ${products.length} products`); ``` ### Product details ```typescript theme={null} const detailResult = await pollJob( await henry.products.details({ link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', }), (args) => henry.products.pollDetails(args), ); const product = detailResult.result; console.log(product?.name, product?.price); ``` ### Headless checkout Purchase is async too - Henry places the order with the merchant(s) in the background. ```typescript theme={null} const purchaseResult = await pollJob( await henry.cart.checkout.purchase('crt_sa2aEsCz9PRM', { buyer: { name: { firstName: 'Jane', lastName: 'Doe' }, email: 'jane@example.com', phone: '+19175551234', shippingAddress: { line1: '350 5th Ave', city: 'New York', province: 'NY', postalCode: '10118', countryCode: 'US', }, card: { nameOnCard: { firstName: 'Jane', lastName: 'Doe' }, details: { cardToken: 'card_live_...', }, }, }, }), (args) => henry.cart.checkout.pollPurchase(args), { intervalMs: 2000, maxAttempts: 30 }, ); if (purchaseResult.status === 'complete' || purchaseResult.status === 'partially_complete') { const { total } = purchaseResult.result!.costs; console.log(`Order placed - total: ${total.value} ${total.currency}`); } ``` *** ## Tips The `pollJob` helper defaults to 1s intervals and 60 attempts, which works for most cases. Product search and details typically complete in 2–5 seconds; orders take longer since Henry places them with the merchant. For order fulfillment, consider skipping polling entirely - [Webhooks](/v1/sdk/server/guides/webhooks) let Henry push results to you instead. *** ## Next steps Skip polling for order events - have Henry push results to your server instead List and filter orders after checkout # Product Discovery Source: https://docs.henrylabs.ai/v1/sdk/server/guides/product-discovery Search any merchant catalog and fetch rich product details - variants, pricing, images, and availability ## Overview Use product discovery to power search bars, product detail pages, and catalog browsers across any merchant on the internet. Both `products.search` and `products.details` are **async jobs** - they return a `refId`, and results arrive once the background work completes. See [Polling](/v1/sdk/server/guides/polling) or [Webhooks](/v1/sdk/server/guides/webhooks) for how to handle the async pattern. ```mermaid theme={null} sequenceDiagram participant App participant Henry App->>Henry: products.search() / products.details() Henry-->>App: { refId, status: "pending" } loop Poll until complete App->>Henry: products.poll*({ refId }) Henry-->>App: { status: "processing" | "complete" | "failed" } end Henry-->>App: { status: "complete", result: { ... } } ``` *** ## Prerequisites ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` *** ## Search the catalog Call `products.search` with a text query and optional filters. Returns immediately with a `refId`. ```typescript theme={null} const search = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max', country: 'US', price: { min: 50, max: 300, currency: 'USD' }, sortBy: 'price-low-to-high', // or 'price-high-to-low' supportedOnly: true, // only checkout-supported merchants merchants: ['nike.com', 'footlocker.com'], // optional: restrict to these merchants }, limit: 20, // 1–100, default 20 mode: 'async', // or 'sync' to wait up to 30s for results }); console.log(search.refId, search.status); // → "prs-ref_3fa8..." "pending" ``` Use `products.pollSearch` to check status until it's `"complete"` or `"failed"`. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper and strategies. ```typescript theme={null} const result = await henry.products.pollSearch({ refId: search.refId }); // result.status: "pending" | "processing" | "complete" | "failed" ``` ```typescript theme={null} const { products, pagination } = result.result!; for (const product of products) { console.log({ name: product.name, price: `${product.price.value} ${product.price.currency}`, merchant: product.merchant, link: product.link, // use this as the cart item `link` inStock: product.availability === 'in_stock', }); } // Paginate with the cursor if (pagination.nextCursor) { const nextPage = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max' }, cursor: Number(pagination.nextCursor), }); } ``` *** ## Fetch product details If you need more granular product information, such as available options and pricing, call `products.details` with the `link` from search for the full enriched payload. ```typescript theme={null} const detailJob = await henry.products.details({ link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', country: 'US', // optional default country selectedOptions: ['regular'], // optional - resolve the next option axis (positional) skipCache: false, // true to bypass cached details mode: 'async', // or 'sync' to wait up to 30s for results }); console.log(detailJob.refId, detailJob.status); // → "prd-ref_3fa8..." "pending" ``` Use `products.pollDetails` to check status. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper. ```typescript theme={null} const detail = await henry.products.pollDetails({ refId: detailJob.refId }); // detail.status: "pending" | "processing" | "complete" | "failed" ``` ```typescript theme={null} const product = detail.result; console.log({ name: product?.name, price: product?.price, options: product?.options, // recursive option tree: per-value availability, link, price featuredImage: product?.images?.find(i => i.isFeatured)?.url, affiliateLink: product?.affiliateLink, // monetized link, when available }); ``` `availability` is one of `in_stock`, `limited_stock`, `out_of_stock`, `preorder`, `backorder`, or `discontinued`. `out_of_stock` is temporary - keep the product and re-check it later. `discontinued` means the merchant no longer offers the product, or the specific variant your link names - drop it from your catalog rather than re-polling. A `discontinued` response carries no `price`, `sku`, or `images`: Henry never substitutes a surviving variant's figures for one the merchant removed. The `options` tree still lists what the merchant sells now, with each value's own images. Each value in the `options` tree carries its own `availability`, a `link` naming that exact variant, and - where the merchant exposes it - its own `price` / `originalPrice`. Variants of one product do not always share a price, and the product-level `price` is the lowest in-stock figure for the featured variant group, so a catalog sync should prefer per-variant prices from the option tree and fall back to the product-level `price` only when a value carries none. One details call on the base product URL returns the whole tree - no per-variant calls needed. Henry caches product details internally. If details for a `link` are already fresh, `pollDetails` will return `"complete"` on the very first call. ### Affiliate links When one of Henry's affiliate networks covers the merchant, the result includes an `affiliateLink` — a Henry-branded URL (`https://api.henrylabs.ai/v1/go/...`) that redirects through the affiliate network to the same product. Commissions from purchases made through it attribute to your app, and end-users see a `henrylabs.ai` link instead of a raw tracking domain. The two link fields have distinct jobs: * **`link`** — the merchant's product URL. Use this for every API call (`products.details`, cart items, and so on). * **`affiliateLink`** — a Henry-branded URL for **end-users to click** (in your UI, emails, or chat responses). Never pass it back to the API: it redirects through the affiliate network's tracking domain rather than resolving to the merchant, so Henry can't resolve products from it. The field is omitted when no network covers the merchant. `products.details` rejects affiliate/tracking URLs in `link` with a validation error — `wild.link` and other known network redirect domains. If you stored an `affiliateLink`, keep the `link` from the same response alongside it for API calls. *** ## Image search Search by image instead of text. Pass a URL, data URL, or base64 payload to find visually similar products. Results combine web-wide visual matches with exact and similar products from Henry's own product-image catalog. With `supportedOnly: true`, results come only from Henry's catalog, so every product returned can be bought through Henry. ```typescript theme={null} // Search with an image URL const imageSearch = await henry.products.search({ type: 'global', filters: { type: 'image', imageUrl: 'https://static.nike.com/a/images/t_web_pdp_535_v2/f_auto/example.png', country: 'US', }, limit: 10, mode: 'async', }); // Poll the same way as text search const result = await henry.products.pollSearch({ refId: imageSearch.refId }); ``` You can also pass a base64-encoded image: ```typescript theme={null} const imageSearch = await henry.products.search({ type: 'global', filters: { type: 'image', imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...', country: 'US', }, limit: 5, }); ``` ### Search one merchant by image Add `filters.imageUrl` to a merchant search to find visually similar products in that merchant's catalog. Results come only from Henry's product-image catalog, so they're available for merchants whose catalog Henry has prepared for image search. Other merchants return a `400` error. ```typescript theme={null} const merchantImageSearch = await henry.products.search({ type: 'merchant', filters: { merchant: 'zara.com', imageUrl: 'https://static.zara.net/photos/example.jpg', }, limit: 10, mode: 'sync', }); ``` *** ## Sync mode All async operations support `mode: "sync"`, which waits up to 30 seconds for the operation to complete before returning. This can simplify your code for quick lookups by avoiding polling entirely. ```typescript theme={null} // Sync mode - returns results directly (no polling needed) const search = await henry.products.search({ type: 'global', filters: { type: 'text', query: 'Nike Air Max' }, limit: 5, mode: 'sync', }); // If the search completed within 30s, results are already here if (search.status === 'complete') { console.log(search.result?.products); } ``` If the operation takes longer than 30 seconds, sync mode returns with a non-terminal status and you'll need to poll normally. For operations that may take longer (e.g. large searches), use `mode: "async"` with [Polling](/v1/sdk/server/guides/polling). *** ## Merchant search Use `type: 'merchant'` to search within a specific merchant's catalog. Pass the merchant name, host, or any URL from that merchant. ```typescript theme={null} const search = await henry.products.search({ type: 'merchant', filters: { merchant: 'nike.com', options: { size: 'M', color: 'black' }, // variant filters - values or option codes }, query: 'running shoes', // optional text query within the merchant limit: 20, mode: 'sync', }); ``` Merchant search accepts a case-insensitive name (e.g. `"Nike"`), a host (e.g. `"nike.com"`), or any URL from the merchant. When `query` is omitted, the merchant's catalog browse is returned. *** ## Search parameters reference ### Top-level parameters | Parameter | Type | Description | | - | - | - | | `type` | `"global" \| "merchant"` | **Required.** `"global"` for cross-merchant search, `"merchant"` for single-merchant | | `filters` | `object` | **Required.** Search filters (see below) | | `query` | `string` | Optional text query (merchant search only) | | `limit` | `number` | Results per page - `1` to `100`, default `20` | | `cursor` | `integer` | Pagination cursor from a previous response | | `mode` | `"async" \| "sync"` | `"async"` (default) returns immediately; `"sync"` waits up to 30s | | `skipCache` | `boolean` | Bypass cached responses and force a fresh search. Default `false` | ### Global search filters (`type: 'global'`) | Parameter | Type | Description | | - | - | - | | `filters.type` | `"text" \| "image"` | **Required.** Text query or image-based search | | `filters.query` | `string` | **Required for text.** Full-text search term | | `filters.imageUrl` | `string` | **Required for image.** HTTP URL, data URL, or base64 payload | | `filters.country` | `string` | ISO country code (e.g. `"US"`, `"JP"`) | | `filters.price.min` | `number` | Minimum price, inclusive (text search only). A page may contain fewer than `limit` results | | `filters.price.max` | `number` | Maximum price, inclusive (text search only) | | `filters.price.currency` | `string` | Currency of the bounds (e.g. `"USD"`). Items in another currency are not filtered out; items with no price are kept under a max-only bound but never match a min | | `filters.sortBy` | `"price-low-to-high" \| "price-high-to-low"` | Sort by price (text search only) | | `filters.supportedOnly` | `boolean` | Only return products from merchants where checkout is supported (`coverageStatus: "supported"` - see [Merchants](/v1/sdk/server/guides/merchant)). Applied after the search runs, so a page may contain fewer than `limit` results | | `filters.merchants` | `string[]` | Restrict a text search to these merchants (1–50), as hosts or URLs (e.g. `nike.com`). Entries that match no merchant are skipped; if none match, the request returns a 400. A page may contain fewer than `limit` results - page until `nextCursor` is null | Global search results are ordered by merchant checkout coverage: products from `supported` merchants rank first, then `testing`, then `unsupported` (the search engine's order is preserved within each tier). Passing `sortBy` disables this ranking - an explicit price sort is kept as-is. To exclude non-supported merchants entirely, set `filters.supportedOnly: true`. ### Merchant search filters (`type: 'merchant'`) | Parameter | Type | Description | | - | - | - | | `filters.merchant` | `string` | **Required.** Merchant name, host, or URL (e.g. `"nike.com"`) | | `filters.country` | `string` | ISO country code - especially important for region-specific merchants (e.g. `"AU"` for priceline.com.au) | | `filters.price.min` | `number` | Minimum price, inclusive | | `filters.price.max` | `number` | Maximum price, inclusive | | `filters.price.currency` | `string` | Currency of the bounds (e.g. `"USD"`). Items in another currency are not filtered out; items with no price are kept under a max-only bound but never match a min | | `filters.sortBy` | `"price-low-to-high" \| "price-high-to-low"` | Sort results by price. Best-effort - honored only by merchants whose search method supports sorting; ignored otherwise | | `filters.options` | `object` | Variant-option filters as name → value pairs (e.g. `{ "size": "M", "color": "black" }`). Accepts displayed values or merchant option codes - see below. Best-effort - honored for merchants served from Henry's search index; ignored by live search methods | | `filters.includeOutOfStock` | `boolean` | Keep sold-out products in the results. Default `false` - see below | | `filters.imageUrl` | `string` | Search this merchant's catalog by image instead of text. HTTP URL, data URL, or base64 payload. Available for merchants whose catalog Henry has prepared for image search; others return a `400` error. `query`, `sortBy` and `cursor` don't apply | Sold-out products are dropped by default. Only the explicit cannot-buy states are dropped - `out_of_stock` and `discontinued` (the merchant no longer offers the product at all): products whose availability is unknown, and products that are `limited_stock`, `preorder`, or `backorder`, are always returned. Where the filter runs depends on the merchant. For merchants served from Henry's search index it runs inside the query, so `limit` counts products you can actually buy. The Shopify catalog search methods apply it after the page is cut, so a page may contain fewer than `limit` results - **page until `nextCursor` is `null`, not until a page comes back short.** Best-effort: other live search methods return whatever the merchant lists. `filters.options` values are case-insensitive and accept either the option value the merchant displays (`"M"`, `"29"`, `"black"`) or the merchant's own option code as returned by [Product Details](#fetch-product-details) (`"size-6000"`, `"color-001"`) - both match the same products. Matching is product-level: a product matches when any of its variants carries the value, which may be a different variant than the one its `link` resolves to. Don't guess option names and values - discover them. `merchants.retrieve` returns a `searchFilters` field with the exact filter axes each merchant's catalog supports. See [Discover search filters](/v1/sdk/server/guides/merchant#discover-search-filters). *** ## Error handling | Error | Cause | Resolution | | - | - | - | | `401 Unauthorized` | Invalid API key | Check `HENRY_SDK_API_KEY` | | `400 Bad Request` | Missing `query` or invalid filter values | Validate input before calling | | `status: "failed"` | Job error - check `error` field | Log and retry | *** ## Next steps Use the product `link` to add items to a cart Browse supported merchants beforehand # Universal Cart Source: https://docs.henrylabs.ai/v1/sdk/server/guides/universal-cart Create and manage multi-merchant carts: add, update, remove items, and get a hosted checkout URL in one call ## Overview The Henry cart is a persistent server-side object that holds items from **any merchant** in a single checkout experience. Creating a cart immediately returns both a `cartId` and a `checkoutUrl` - there's no separate step to generate a checkout link. *** ## Prerequisites ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY!, }); ``` *** ## Create a cart `cart.create` is the starting point. Pass an array of items using product `link` URLs (from search results or any product page), and optionally configure cart settings and tags. Each item can also include an `affiliateProductLink`, selected options, shipping, coupons, and metadata. ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: "https://www.nike.com/u/custom-nike-ja-3-by-you-10002205", affiliateProductLink: "https://partner.example.com/click?url=https%3A%2F%2Fwww.nike.com%2Fu%2Fcustom-nike-ja-3-by-you-10002205", quantity: 2, selectedOptions: ["regular", "black", "10-w"], metadata: { creatorSource: "Frank Herbert" }, }, ], settings: { options: { allowPartialPurchase: true, collectBuyerEmail: "required", collectBuyerAddress: "optional", }, serviceFeeFixed: { value: 1.99, currency: "USD" }, serviceFeePercent: 10, events: [], }, tags: { region: "us-ny" }, }); const { cartId, checkoutUrl } = cart.data; console.log("Cart ID:", cartId); console.log("Checkout URL:", checkoutUrl); ``` The `checkoutUrl` is your hosted checkout link - share it with your user directly. See [Checkout](/v1/sdk/server/guides/checkout) for how to embed it. ### Cart creation response ```json theme={null} { "success": boolean, "status": "string", "message": "string", "data": { "cartId": "string", "checkoutUrl": "https://example.com", "data": { "items": [ { "link": "https://www.nike.com/u/custom-nike-ja-3-by-you-10002205", "affiliateProductLink": "https://partner.example.com/click?url=https%3A%2F%2Fwww.nike.com%2Fu%2Fcustom-nike-ja-3-by-you-10002205", "quantity": 2, "selectedOptions": ["regular", "black", "10-w"], "selectedShipping": { "value": "standard" }, "coupons": ["SUMMER26", "SAVE10"], "metadata": { "creatorSource": "Frank Herbert" } } ], "settings": { "options": { "allowPartialPurchase": true, "collectBuyerEmail": "required", "collectBuyerAddress": "optional" }, "serviceFeeFixed": { "value": 1.99, "currency": "USD" }, "serviceFeePercent": 10, "events": [ { "type": "order", "conditional": { "type": "tier", "operator": "equals", "value": "string" }, "data": [ { "type": "give_points_fixed", "points": 1 } ] } ] }, "tags": { "region": "us-ny" } }, "metadata": {} } } ``` *** ## Manage cart items ### Add an item Add a product to an existing cart. Use the product `link` URL as the identifier. ```typescript theme={null} const updated = await henry.cart.item.add(cartId, { item: { link: "https://www.adidas.com/us/ultraboost-22-shoes/GX5591.html", affiliateProductLink: "https://partner.example.com/click?url=https%3A%2F%2Fwww.adidas.com%2Fus%2Fultraboost-22-shoes%2FGX5591.html", quantity: 1, selectedOptions: ["9.5", "Core Black"], selectedShipping: { value: "standard" }, coupons: ["SAVE10"], metadata: { giftWrap: true }, }, }); console.log(updated.data.data.items.length); // cart now has 2 items ``` ### Update an item quantity ```typescript theme={null} const updated = await henry.cart.item.update(cartId, { item: { link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', quantity: 3, // set new quantity }, }); ``` Setting `quantity` to `0` via `cart.item.update` also removes the item from the cart. ### Remove an item ```typescript theme={null} const updated = await henry.cart.item.remove(cartId, { link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', }); console.log(updated.data.data.items.length); // one fewer item ``` *** ## Fetch carts Henry exposes two retrieval methods with different shapes: | Method | Returns | Use when | | - | - | - | | `cart.list({ cartId?, tags?, limit? })` | Array of carts | Looking up by tags, or paging through many carts | | `cart.fetch(cartId, { buyer? })` | A single cart | You know the `cartId` and optionally want to prefill checkout | ### List carts `limit` accepts 1-100 (default 20). ```typescript theme={null} // Fetch a specific cart by ID const { data: carts } = await henry.cart.list({ cartId }); const cart = carts[0]; console.log(cart.data.items); console.log(cart.checkoutUrl); // still valid - use it any time // Filter carts by tags const { data: userCarts } = await henry.cart.list({ tags: { userId: 'user_123' }, }); ``` ### Fetch a single cart (with optional buyer prefill) `cart.fetch` returns one cart by id. Pass an optional `buyer` to prefill the hosted checkout - the returned `checkoutUrl` will embed the buyer details as query params so fields render pre-filled when the user opens it. ```typescript theme={null} // Plain fetch by id const { data: cart } = await henry.cart.fetch(cartId); console.log(cart.data.items, cart.checkoutUrl); // Fetch + prefill the hosted checkout const { data: prefilled } = await henry.cart.fetch(cartId, { buyer: { name: { firstName: 'Jane', lastName: 'Doe' }, email: 'jane@example.com', phone: '+15551234567', shippingAddress: { name: { firstName: 'Jane', lastName: 'Doe' }, line1: '350 5th Ave', city: 'New York', province: 'NY', postalCode: '10118', countryCode: 'US', }, }, }); // prefilled.checkoutUrl now renders with Jane's details pre-populated ``` `buyer` is purely a UX convenience for hosted checkout - it does not persist on the cart. To actually commit buyer info, use [Headless Checkout](/v1/sdk/server/guides/checkout#headless-checkout) via `cart.checkout.purchase`. *** ## Delete a cart Remove a cart entirely when it's no longer needed (e.g. on session logout). ```typescript theme={null} await henry.cart.delete(cartId); ``` *** ## Cart item fields reference | Field | Type | Required | Description | | - | - | - | - | | `link` | `string` | Y | Direct product URL from any merchant | | `affiliateProductLink` | `string` | - | Optional affiliate or redirect URL used for fulfillment. If omitted, `link` is used. | | `quantity` | `number` | - | Defaults to `1` | | `selectedOptions` | `string[]` | - | Ordered array of option values e.g. `['regular', 'black', '10-w']` | | `selectedShipping` | `{ id?, value? }` | - | Preferred shipping method | | `coupons` | `string[]` | - | Coupon codes to apply at checkout | | `metadata` | `object` | - | Arbitrary key/value data passed through to orders | *** ## Cart tags Attach key-value `tags` when creating a cart to associate it with your own identifiers (e.g. user IDs, regions, campaigns). You can then filter carts by tags with `cart.list`. ```typescript theme={null} const cart = await henry.cart.create({ items: [{ link: '...', quantity: 1 }], tags: { userId: 'user_123', campaign: 'summer-sale' }, }); // Later, retrieve all carts for this user const { data: carts } = await henry.cart.list({ tags: { userId: 'user_123' }, }); ``` *** ## Cart settings reference | Field | Type | Description | | - | - | - | | `options.allowPartialPurchase` | `boolean` | Let buyers remove items during checkout | | `options.collectBuyerEmail` | `"off" \| "required" \| "optional"` | Email collection behavior | | `options.collectBuyerAddress` | `"off" \| "required" \| "optional"` | Address collection behavior | | `options.collectBuyerPhone` | `"off" \| "required" \| "optional"` | **Deprecated, ignored.** Phone is always required | | `serviceFeePercent` | `number` | Service fee as a percentage of the order total (0-100) | | `serviceFeeFixed` | `{ value, currency }` | Fixed service fee amount added to the order | | `events` | `CartEvent[]` | Triggers to fire when cart events occur | *** ## Cart events `settings.events` lets you attach triggers to cart lifecycle events. Each entry declares an event `type` to listen for, an optional `conditional`, and a list of actions (`data`) to fire when it occurs. ### Supported event types #### Order events | Event type | Fires when | | - | - | | `order` | Any order event | | `order.purchase` | Any purchase event (full or partial, any outcome) | | `order.purchase.pending` | Purchase created, awaiting payment confirmation | | `order.purchase.processing` | Payment confirmed, placing with merchants | | `order.purchase.complete` | Every item was purchased | | `order.purchase.partially_complete` | Some items were purchased, the rest failed | | `order.purchase.failed` | No items were purchased | | `order.purchase.cancelled` | Purchase cancelled | | `order.purchase.full` | Legacy — see below | | `order.purchase.full.pending` | Purchase pending (legacy) | | `order.purchase.full.processing` | Purchase processing (legacy) | | `order.purchase.full.complete` | Every item was purchased (legacy) | | `order.purchase.full.failed` | No items were purchased (legacy) | | `order.purchase.full.cancelled` | Purchase cancelled (legacy) | | `order.purchase.partial` | Legacy — see below | | `order.purchase.partial.pending` | Partial purchase pending (legacy) | | `order.purchase.partial.processing` | Partial purchase processing (legacy) | | `order.purchase.partial.complete` | Some items were purchased (legacy) | | `order.purchase.partial.failed` | Some items failed (legacy) | | `order.purchase.partial.cancelled` | Partial purchase cancelled (legacy) | | `order.item` | Any item-level event | | `order.item.pending` | Individual item pending | | `order.item.processing` | Individual item processing | | `order.item.complete` | Individual item placed successfully | | `order.item.failed` | Individual item failed | | `order.update` | Any order update event | | `order.update.3ds-required` | 3DS authentication required | | `order.update.adding-to-cart` | Adding item to merchant cart | | `order.update.filling-address-details` | Filling in address details | | `order.update.filling-card-details` | Filling in card details | | `order.update.submitting-order` | Submitting the order | | `order.update.changing-quantity` | Changing item quantity | | `order.update.applying-coupon` | Applying a coupon code | | `order.update.selecting-shipping` | Selecting shipping option | | `order.update.selecting-options` | Selecting product options | | `order.update.product-search` | Searching the merchant for the product | A purchase settles once, and fires exactly one of `order.purchase.complete`, `order.purchase.partially_complete` or `order.purchase.failed`. After an item fails, the event can wait up to an hour before it fires, so it always reports the final outcome. Until then the order and that item read `processing` in every event payload, as they do when polled. The `full` and `partial` events are kept for existing integrations. A settled order fires them alongside the event above: `complete` also fires `order.purchase.full.complete`, `partially_complete` also fires `order.purchase.partial.complete` and `order.purchase.partial.failed`, and `failed` also fires `order.purchase.full.failed`. Use the three settled events for new integrations. #### Points & tier events | Event type | Fires when | | - | - | | `points` | Any points event | | `points.give` | Points awarded to a buyer | | `points.remove` | Points removed from a buyer | | `tier` | Any tier event | | `tier.give` | A tier assigned to a buyer | | `tier.remove` | A tier removed from a buyer | ### Supported action types | Action type | Description | Required fields | | - | - | - | | `send_webhook` | POST the event payload to a registered webhook | `webhookUUID` | | `give_points_fixed` | Award a fixed number of points to the buyer | `points` | | `give_points_per_spent` | Award points proportional to the order amount | `points`, `perAmount` | | `remove_points_fixed` | Deduct a fixed number of points from the buyer | `points` | | `remove_points_per_spent` | Deduct points proportional to the order amount | `points`, `perAmount` | | `give_tier` | Assign a loyalty tier to the buyer | `tierUUID` | | `remove_tier` | Remove the buyer's current tier | - | ### Example - webhook on successful purchase ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: 'https://www.nike.com/t/air-max-270-shoes/AH8050-002', quantity: 1, }, ], settings: { events: [ { type: 'order.purchase.full.complete', data: [ { type: 'send_webhook', webhookUUID: 'c5dbd16b-60f1-42c7-8d36-a55c46e7a8c6', }, ], }, ], }, }); ``` ### Example - award points on purchase ```typescript theme={null} settings: { events: [ { type: 'order.purchase.full.complete', data: [ { type: 'give_points_per_spent', points: 1, perAmount: { value: 1, currency: 'USD' }, // 1 point per $1 spent }, ], }, ], } ``` ### Conditionals Add a `conditional` to a trigger to gate it on the buyer's current tier or points balance: ```typescript theme={null} { type: 'order.purchase.full.complete', conditional: { type: 'tier', operator: 'equals', // 'equals' | 'not_equals' value: 'your-tier-uuid', }, data: [{ type: 'give_points_fixed', points: 500 }], } ``` Register and manage webhooks in the [Henry Dashboard](https://app.henrylabs.ai). The `webhookUUID` is the identifier assigned when you create a webhook endpoint. See [Webhooks](/v1/sdk/server/guides/webhooks) for payload shape, verification, and the full event reference. *** ## Error handling | Error | Cause | | - | - | | `400 Bad Request` | Missing required fields or invalid URL in `link` | | `401 Unauthorized` | Invalid API key | | `404 Not Found` | `cartId` does not exist or belongs to a different app | *** ## Next steps Once your cart is ready, proceed to checkout: Embed the hosted checkout or call the headless purchase API Track and list orders after checkout completes # Common Use Cases Source: https://docs.henrylabs.ai/v1/sdk/server/guides/use-cases End-to-end recipes that combine product search, details, and checkout for the patterns app developers reach for most. ## Overview Henry's server SDK exposes three primitives - `products.search`, `products.details`, and the `cart.*` family - that compose into a handful of recurring app patterns. Pick the flow below that matches where your users **start**: | If your users start from... | Use flow | | - | - | | A text query | [Shopping agent / chatbot](#flow-1-shopping-agent--chatbot) | | A photo or screenshot | [Visual / "shop the look" app](#flow-2-visual--shop-the-look-app) | | A merchant catalog feed | [Product recommendation feed](#flow-3-product-recommendation-feed) | | A merchant-scoped storefront | [Embedded merchant storefront](#flow-4-embedded-merchant-storefront) | | A known product URL | ["Buy with Henry" from any link](#flow-5-buy-with-henry-from-any-link) | Every flow ends the same way - a Henry cart with a `checkoutUrl` you can hand to your user, or a headless `cart.checkout.purchase` call that places the order from your server. *** ## Building blocks ### The four search modes `products.search` is one endpoint with four useful shapes, switched by `type` and `filters.type`: | Mode | Shape | When to reach for it | | - | - | - | | Global text | `type: 'global'`, `filters: { type: 'text', query }` | Discovery across the open web | | Global image | `type: 'global'`, `filters: { type: 'image', imageUrl }` | Visual search from a photo / screenshot | | Merchant text | `type: 'merchant'`, `filters: { merchant }`, optional top-level `query` | Search within one merchant's catalog | | Merchant browse | `type: 'merchant'`, `filters: { merchant }`, **no** `query` | Paginated browse of a merchant's full catalog | ### Variant resolution on details `products.details` returns an `options` tree under `result.options`. Each option's `values[].nextOption` is **recursive** - selecting one value reveals the next axis (e.g. size → colour → length). To walk it: 1. Call `products.details` with `link`. 2. Read `result.options` to discover the first axis (say, "fit"). 3. Re-call `products.details` with `selectedOptions: ['regular']` to surface the next axis (say, "colour"). 4. Re-call with `selectedOptions: ['regular', 'black']` for the next (size), etc. The `selectedOptions` array is **positional** - values are applied in order against the option chain. The same array can be passed as `selectedOptions` on the cart item when you're ready to add it. ### Async vs sync Every async operation (`products.search`, `products.details`, `cart.checkout.details`, `cart.checkout.purchase`) accepts `mode: 'async' | 'sync'`. Sync waits up to 30 seconds; async returns immediately with a `refId` you poll via the matching `poll*` helper. See [Polling](/v1/sdk/server/guides/polling) for the reusable helper - this guide assumes it's imported as `pollJob`. ```typescript theme={null} import HenrySDK from '@henrylabs/sdk'; import { pollJob } from './utils/henry-poll'; const henry = new HenrySDK({ apiKey: process.env.HENRY_SDK_API_KEY! }); ``` *** ## Flow 1 - Shopping agent / chatbot **Use this when** your app takes a free-text user intent ("find me a quiet hairdryer under \$200") and returns purchasable products. Typical surfaces: AI assistants, conversational commerce, MCP tools. ```mermaid theme={null} sequenceDiagram participant User participant App participant Henry User->>App: "find me a quiet hairdryer under $200" App->>Henry: products.search (global text + price filter) Henry-->>App: products[] App->>Henry: products.details (top pick) Henry-->>App: variants + availability App->>Henry: cart.create Henry-->>App: cartId + checkoutUrl App-->>User: "Here's the checkout link" ``` Use `type: 'global'` with `filters.type: 'text'` and any of `country`, `price`, `sortBy`. ```typescript theme={null} const search = await pollJob( await henry.products.search({ type: 'global', filters: { type: 'text', query: 'quiet hairdryer', country: 'US', price: { max: 200, currency: 'USD' }, sortBy: 'price-low-to-high', }, limit: 10, }), (args) => henry.products.pollSearch(args), ); const products = search.result!.products; ``` Search results are catalog-grade. For prices, availability, and variants accurate enough to commit to, fetch details on the candidate you want. ```typescript theme={null} const top = products[0]; const detail = await pollJob( await henry.products.details({ link: top.link, country: 'US' }), (args) => henry.products.pollDetails(args), ); if (detail.result?.availability !== 'in_stock') { // skip to the next candidate } ``` ```typescript theme={null} const cart = await henry.cart.create({ items: [{ link: top.link, quantity: 1 }], tags: { sessionId: 'chat_session_abc' }, }); return cart.data.checkoutUrl; ``` Cart `tags` are perfect for tying a checkout back to a conversation or user. Filter on them later with `cart.list({tags})`. *** ## Flow 2 - Visual / "shop the look" app **Use this when** your users upload or screenshot something they want to buy. Typical surfaces: Lens-style apps, social shopping, photo-to-buy. `filters.imageUrl` accepts an HTTP(S) URL, a `data:` URL, or a raw base64 payload. Results route through `shopping.google.com` regardless of which merchant ends up matching. ```typescript theme={null} const search = await pollJob( await henry.products.search({ type: 'global', filters: { type: 'image', imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUh...', country: 'US', }, limit: 12, }), (args) => henry.products.pollSearch(args), ); const matches = search.result!.products; ``` Image search produces visually-similar candidates - present them as a grid, then fetch full details once the user commits. ```typescript theme={null} const picked = matches.find((p) => p.link === userPickedLink)!; const detail = await pollJob( await henry.products.details({ link: picked.link }), (args) => henry.products.pollDetails(args), ); ``` ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: picked.link, quantity: 1, // selectedOptions only needed once you've walked the option tree }, ], }); window.location.href = cart.data.checkoutUrl; ``` Price filters (`filters.price.min/max`), `sortBy`, and the `filters.merchants` list are text-search only. Image search returns visually-similar matches, ordered by relevance. *** ## Flow 3 - Product recommendation feed **Use this when** you're powering a recommendation or deal feed by mirroring a merchant's catalog. A periodic crawl plus change detection lets you surface fresh picks, price drops, or restocks to your users. Typical surfaces: recommendation feeds, deal sites, comparison engines, RSS-style product feeds, price trackers. The trick: `products.search` in merchant mode with **no** `query` returns the merchant's broad catalog. Cache the cheap fields (`link`, `price`, `availability`), then only spend a `products.details` call on items that actually changed. ```typescript theme={null} let cursor: number | undefined = undefined; do { const page = await pollJob( await henry.products.search({ type: 'merchant', filters: { merchant: 'nike.com' }, // omit `query` — this is a catalog browse, not a search limit: 100, cursor, }), (args) => henry.products.pollSearch(args), ); const { products, pagination } = page.result!; await upsertCatalogRows(products); // your DB cursor = pagination.nextCursor ?? undefined; } while (cursor !== undefined); ``` Compare each row against your cached snapshot. Re-fetch details when `price` moves or `availability` flips. ```typescript theme={null} const changed = await diffAgainstCache(catalogRows); // your logic for (const row of changed) { const detail = await pollJob( await henry.products.details({ link: row.link }), (args) => henry.products.pollDetails(args), ); if (detail.result) { await notifySubscribers({ link: row.link, name: detail.result.name, oldPrice: row.previousPrice, newPrice: detail.result.price, }); } } ``` When a subscriber clicks through, the link they already have on the alert is the same `link` that goes into `cart.create` - no extra search needed. ```typescript theme={null} const cart = await henry.cart.create({ items: [{ link: alert.productLink, quantity: 1 }], }); return cart.data.checkoutUrl; ``` Use `limit: 100` (the maximum) on browse calls to minimise the number of paginated requests, and persist `pagination.nextCursor` between crawl runs to resume mid-catalog instead of starting over. *** ## Flow 4 - Embedded merchant storefront **Use this when** you're building a branded buy-button or niche storefront scoped to a specific merchant - your user starts inside one brand and you want to walk them through size / colour / length variants. Typical surfaces: white-label merchant search, creator-led storefronts, brand-specific buy-buttons. This is the flow where `selectedOptions` does the heaviest lifting. ```typescript theme={null} const search = await pollJob( await henry.products.search({ type: 'merchant', filters: { merchant: 'nike.com' }, query: 'running shoes', limit: 20, }), (args) => henry.products.pollSearch(args), ); const candidate = search.result!.products[0]; ``` Each `products.details` call surfaces the next axis. Re-call as the user picks values, feeding the running array back in. ```typescript theme={null} // Initial - discover the first axis (e.g. "fit") let detail = await pollJob( await henry.products.details({ link: candidate.link }), (args) => henry.products.pollDetails(args), ); const fitAxis = detail.result?.options; // fitAxis.values[].value: 'regular' | 'wide' | ... // User picks "regular" — surface the next axis ("colour") detail = await pollJob( await henry.products.details({ link: candidate.link, selectedOptions: ['regular'], }), (args) => henry.products.pollDetails(args), ); const colourAxis = detail.result?.options; // User picks "black" — surface the final axis ("size") detail = await pollJob( await henry.products.details({ link: candidate.link, selectedOptions: ['regular', 'black'], }), (args) => henry.products.pollDetails(args), ); const sizeAxis = detail.result?.options; ``` ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: candidate.link, quantity: 1, selectedOptions: ['regular', 'black', '10-w'], }, ], }); return cart.data.checkoutUrl; ``` When `result.options.status === 'unknown'` Henry couldn't determine variants for that product - send the user straight to cart with the `link` only and let the hosted checkout collect any remaining selections. *** ## Flow 5 - "Buy with Henry" from any link **Use this when** the user (or your agent) already has a product URL and you want to skip search entirely. Typical surfaces: browser extensions, recipe sites, social-post buy-buttons, AI agents that receive a URL directly. ```mermaid theme={null} sequenceDiagram participant User participant App participant Henry User->>App: clicks "Buy" on a product URL App->>Henry: products.details (optional - preview) App->>Henry: cart.create Henry-->>App: cartId + checkoutUrl App-->>User: opens hosted checkout ``` Useful if you want to show price / availability / image before sending the user to checkout. ```typescript theme={null} const detail = await pollJob( await henry.products.details({ link: incomingUrl }), (args) => henry.products.pollDetails(args), ); if (detail.result?.availability === 'out_of_stock') { return showOutOfStockUI(detail.result); } ``` ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: incomingUrl, quantity: 1, // selectedOptions optional — hosted checkout will collect them if needed }, ], settings: { options: { collectBuyerEmail: 'required', collectBuyerAddress: 'required', }, }, }); return cart.data.checkoutUrl; ``` If you're collecting card details in your own UI via the [Card Element](/v1/sdk/client/elements/card-element), skip the hosted URL and call `cart.checkout.purchase` directly. See [Headless Checkout](/v1/sdk/server/guides/checkout#headless-checkout) for the full pattern. ```typescript theme={null} const order = await pollJob( await henry.cart.checkout.purchase(cart.data.cartId, { buyer: { name: { firstName: 'Jane', lastName: 'Doe' }, email: 'jane@example.com', phone: '+12125551234', shippingAddress: { line1: '350 5th Ave', city: 'New York', province: 'NY', postalCode: '10118', countryCode: 'US', }, card: { nameOnCard: { firstName: 'Jane', lastName: 'Doe' }, details: { cardToken: 'card_live_...' }, }, }, }), (args) => henry.cart.checkout.pollPurchase(args), { intervalMs: 2000, maxAttempts: 30 }, ); ``` *** ## Error handling & retries Every flow above touches the same async machinery, so the same handling applies: | Symptom | Likely cause | What to do | | - | - | - | | `status: 'failed'` with `error.code` | Merchant-side failure (e.g. captcha, listing pulled) | Log `error`, fall back to the next candidate or surface a "we couldn't fetch this product" UI | | `mode: 'sync'` returns with `pending` / `processing` | Operation didn't finish in the 30s window | Fall back to polling with `pollJob` instead of re-issuing the request | | `result.options.status === 'unknown'` | Variants couldn't be inferred | Drop into cart without `selectedOptions` and let hosted checkout collect them | | `404 Not Found` on `cart.fetch` | `cartId` belongs to a different app, or doesn't exist | Verify the cart was created with the same API key | | `401 Unauthorized` everywhere | `HENRY_SDK_API_KEY` missing or wrong | Check your env | For long-running purchases, prefer [Webhooks](/v1/sdk/server/guides/webhooks) over polling - register a `webhookUUID` on the cart's `settings.events` and Henry will push completion events to you. *** ## Next steps Full reference for `products.search` and `products.details` parameters Cart settings, items, tags, and lifecycle events Hosted iframe / redirect or headless `cart.checkout.purchase` The reusable `pollJob` helper used throughout this guide Skip polling entirely - have Henry push order events to you Look up which merchants are supported before scoping a flow # Webhooks Source: https://docs.henrylabs.ai/v1/sdk/server/guides/webhooks Receive real-time order and fulfillment events pushed directly to your server - no polling required ## Overview Instead of polling for order status, you can configure Henry to push events directly to your server via webhooks. When an order transitions state - placed, completed, cancelled - Henry fires an HTTP POST to your endpoint with a signed payload. Webhooks are configured per-cart via the `settings.events` array on `cart.create`. *** ## How it works ```mermaid theme={null} sequenceDiagram participant App as Your App participant Henry as Henry App->>Henry: cart.create({ items, settings: { events } }) Henry-->>App: { cartId, checkoutUrl } Note over App,Henry: User completes checkout... Henry->>App: POST /webhook
{ event, data, timestamp }
X-Henry-Signature, X-Henry-Timestamp App->>App: Verify signature App-->>Henry: 200 OK ``` *** ## 1. Register a webhook endpoint Go to the [Henry Dashboard](https://app.henrylabs.ai), open your app settings, and add a webhook endpoint URL. Henry will generate a **webhook UUID** and a **webhook secret** for that endpoint. * The **UUID** is used in cart settings to reference the webhook * The **secret** is used to verify incoming requests (keep it private, like an API key) *** ## 2. Configure a cart with webhook events Pass the webhook UUID in `settings.events` when creating a cart. Henry fires that webhook when the specified event occurs. ```typescript theme={null} const cart = await henry.cart.create({ items: [ { link: 'https://www.petbarn.com.au/p/love-em-air-dried-beef-liver-dog-treat/139654', quantity: 1, }, ], settings: { events: [ { type: 'order.purchase', // fire for any update to the purchase data: [ { type: 'send_webhook', webhookUUID: 'c5dbd16b-60f1-42c7-8d36-a55c46e7a8c6', }, ], }, ], }, }); const { checkoutUrl } = cart.data; ``` To fire on specific outcomes, use a more granular event type: ```typescript theme={null} settings: { events: [ { type: 'order.purchase.complete', // every item was purchased data: [{ type: 'send_webhook', webhookUUID: '...' }], }, { type: 'order.purchase.partially_complete', // some items were purchased data: [{ type: 'send_webhook', webhookUUID: '...' }], }, { type: 'order.purchase.failed', // nothing was purchased data: [{ type: 'send_webhook', webhookUUID: '...' }], }, ], } ``` *** ## 3. Receive and verify the webhook Henry sends a `POST` request with three important headers: | Header | Description | | - | - | | `X-Henry-Signature` | HMAC-SHA256 hex signature of `{timestamp}.{body}` | | `X-Henry-Timestamp` | Unix timestamp in milliseconds | | `Content-Type` | `application/json` | **Always verify the signature** before processing the payload. ### Verification logic ```typescript theme={null} import { createHmac, timingSafeEqual } from 'crypto'; const WEBHOOK_SECRET = process.env.HENRY_WEBHOOK_SECRET!; const REPLAY_WINDOW_MS = 5 * 60 * 1000; // 5 minutes function verifyHenryWebhook(rawBody: string, signature: string | null, timestamp: string | null): boolean { if (!signature || !timestamp) return false; const ts = Number(timestamp); // Reject stale requests (replay attack prevention) if (Math.abs(Date.now() - ts) > REPLAY_WINDOW_MS) return false; // Reconstruct the signed payload const signedPayload = `${ts}.${rawBody}`; // Constant-time comparison to prevent timing attacks const expected = createHmac('sha256', WEBHOOK_SECRET).update(signedPayload).digest(); const actual = Buffer.from(signature, 'hex'); return expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual); } ``` Always read the raw request body **before** parsing it as JSON. Parsing first can change the string representation and break signature verification. ### Full server example (Node.js / Express) ```typescript theme={null} import express from 'express'; import { createHmac } from 'crypto'; const app = express(); const WEBHOOK_SECRET = process.env.HENRY_WEBHOOK_SECRET!; // Use raw body parser for the webhook route app.post('/webhook/henry', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); const signature = req.headers['x-henry-signature'] as string; const timestamp = req.headers['x-henry-timestamp'] as string; if (!verifyHenryWebhook(rawBody, signature, timestamp)) { return res.status(401).send('Unauthorized'); } let event: HenryWebhookPayload; try { event = JSON.parse(rawBody); } catch { return res.status(400).send('Invalid JSON'); } // Always respond 200 quickly - process async res.json({ received: true }); handleEvent(event).catch(console.error); }); async function handleEvent(event: HenryWebhookPayload) { console.log(`[${event.timestamp}] ${event.event}`, event.data); switch (event.event) { case 'order.purchase.complete': case 'order.purchase.partially_complete': // Placed — check data.items[].status for which items were bought await fulfillOrder(event.data); break; case 'order.purchase.failed': await notifyUserOfFailure(event.data); break; } } ``` ### Full server example (Bun) ```typescript theme={null} import { timingSafeEqual } from 'crypto'; const WEBHOOK_SECRET = process.env.HENRY_WEBHOOK_SECRET; if (!WEBHOOK_SECRET) throw new Error('HENRY_WEBHOOK_SECRET is required'); const REPLAY_WINDOW_MS = 5 * 60 * 1000; function verifySignature(body: string, signature: string | null, timestamp: string | null): boolean { if (!signature || !timestamp) return false; const ts = Number(timestamp); if (Math.abs(Date.now() - ts) > REPLAY_WINDOW_MS) return false; const signedPayload = `${ts}.${body}`; const hasher = new Bun.CryptoHasher('sha256', WEBHOOK_SECRET); hasher.update(signedPayload); const expected = Buffer.from(hasher.digest('hex'), 'hex'); const actual = Buffer.from(signature, 'hex'); return expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual); } Bun.serve({ port: 6789, async fetch(req) { const url = new URL(req.url); if (url.pathname === '/webhook' && req.method === 'POST') { const body = await req.text(); const signature = req.headers.get('x-henry-signature'); const timestamp = req.headers.get('x-henry-timestamp'); if (!verifySignature(body, signature, timestamp)) { return new Response('Unauthorized', { status: 401 }); } let event: HenryWebhookPayload; try { event = JSON.parse(body); } catch { return new Response('Invalid JSON', { status: 400 }); } // Respond immediately, process async handleEvent(event).catch(console.error); return Response.json({ received: true }); } return new Response('Not Found', { status: 404 }); }, }); ``` *** ## Webhook payload structure Henry sends a consistent envelope for all events. Each delivery is a single attempt with a 10-second timeout and is not retried. ```typescript theme={null} type HenryWebhookPayload = { event: string; // e.g. "order.purchase" timestamp: string; // ISO 8601 data: OrderEventData | PointsEventData | TierEventData; }; ``` ### Example payload ```json theme={null} { "event": "order.purchase.complete", "timestamp": "2025-03-17T14:32:00.000Z", "data": { "status": "complete", "orderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "cartId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "items": [ { "status": "complete", "quantity": 1, "productLink": "https://www.nike.com/t/air-max-270-shoes/AH8050-002", "productRawUrl": "https://www.nike.com/t/air-max-270-shoes/AH8050-002", "merchantName": "Nike", "selectedOptions": ["Black", "10"], "metadata": { "source": "homepage" }, "recoveryUrl": "https://www.nike.com/t/air-max-270-shoes/AH8050-002" } ], "details": { "name": { "firstName": "Jane", "lastName": "Doe" }, "email": "jane@example.com", "phone": "+14155550123", "shippingAddress": { "line1": "1 Main St", "city": "San Francisco", "province": "CA", "postalCode": "94105", "countryCode": "US" } }, "results": { "costs": { "subtotal": { "value": 150.00, "currency": "USD" }, "serviceFee": { "value": 7.50, "currency": "USD" }, "total": { "value": 157.50, "currency": "USD" } } } } } ``` *** ## Event types reference Events follow a dot-notation hierarchy. Subscribing to a parent event fires for all child events. ### Order events A purchase settles once and fires exactly one of `order.purchase.complete` (every item purchased), `order.purchase.partially_complete` (some items purchased) or `order.purchase.failed` (nothing purchased). After an item fails, the event can wait up to an hour before it fires, so it always reports the final outcome. Until then the order and that item read `processing` in every event payload, as they do when polled. The `full` and `partial` events are kept for existing integrations. While the purchase is `pending` / `processing` they say whether every cart item was attempted together. Once it settles they follow the outcome: `complete` also fires `order.purchase.full.complete`, `partially_complete` also fires `order.purchase.partial.complete` and `order.purchase.partial.failed`, and `failed` also fires `order.purchase.full.failed`. | Event type | Fires when | | - | - | | `order` | Any order event | | `order.purchase` | Any purchase event (full or partial, any outcome) | | `order.purchase.pending` | Purchase created, awaiting payment confirmation | | `order.purchase.processing` | Payment confirmed, placing with merchants | | `order.purchase.complete` | Every item was purchased | | `order.purchase.partially_complete` | Some items were purchased, the rest failed | | `order.purchase.failed` | No items were purchased | | `order.purchase.cancelled` | Purchase cancelled | | `order.purchase.full` | Legacy — see above | | `order.purchase.full.pending` | Full purchase pending (legacy) | | `order.purchase.full.processing` | Full purchase processing (legacy) | | `order.purchase.full.complete` | Every item was purchased (legacy) | | `order.purchase.full.failed` | No items were purchased (legacy) | | `order.purchase.full.cancelled` | Full purchase cancelled (legacy) | | `order.purchase.partial` | Legacy — see above | | `order.purchase.partial.pending` | Partial purchase pending (legacy) | | `order.purchase.partial.processing` | Partial purchase processing (legacy) | | `order.purchase.partial.complete` | Some items were purchased (legacy) | | `order.purchase.partial.failed` | Some items failed (legacy) | | `order.purchase.partial.cancelled` | Partial purchase cancelled (legacy) | | `order.item` | Any item-level event | | `order.item.pending` | Individual item pending | | `order.item.processing` | Individual item processing | | `order.item.complete` | Individual item placed successfully | | `order.item.failed` | Individual item failed | | `order.update` | Any order update event | | `order.update.3ds-required` | 3DS authentication required | | `order.update.adding-to-cart` | Adding item to merchant cart | | `order.update.filling-address-details` | Filling in address details | | `order.update.filling-card-details` | Filling in card details | | `order.update.submitting-order` | Submitting the order | | `order.update.changing-quantity` | Changing item quantity | | `order.update.applying-coupon` | Applying a coupon code | | `order.update.selecting-shipping` | Selecting shipping option | | `order.update.selecting-options` | Selecting product options | | `order.update.product-search` | Searching the merchant for the product | `details.phone` and `details.shippingAddress` are included only when the cart collects them. `items[].recoveryUrl` is a link back to the merchant for that item. On `order.purchase.cancelled`, `results` carries `cancelledAt` and `reason` instead of `costs`. ### Points & tier events | Event type | Fires when | | - | - | | `points` | Any points event | | `points.give` | Points awarded to a buyer | | `points.remove` | Points removed from a buyer | | `tier` | Any tier event | | `tier.give` | A tier assigned to a buyer | | `tier.remove` | A tier removed from a buyer | For most use cases, subscribe to `order.purchase.complete`, `order.purchase.partially_complete` and `order.purchase.failed` — exactly one fires per purchase, once its outcome is final. *** ## Conditionals Each event in `settings.events` accepts an optional `conditional` that filters when the trigger fires. Two types are supported: ```typescript theme={null} settings: { events: [ { type: 'order.purchase.complete', conditional: { type: 'tier', operator: 'equals', // 'equals' | 'not_equals' value: 'your-tier-uuid', }, data: [{ type: 'send_webhook', webhookUUID: '...' }], }, { type: 'points.give', conditional: { type: 'points', operator: 'greater_than_or_equal', // 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_than_or_equal' | 'less_than_or_equal' value: 100, }, data: [{ type: 'send_webhook', webhookUUID: '...' }], }, ], } ``` *** ## Next steps Prefer polling for product search and details jobs Configure webhook events when creating a cart # MCP Server Source: https://docs.henrylabs.ai/v1/sdk/server/mcp/remote-mcp Connect any MCP client or agent to Henry's hosted MCP server. ## Overview The Henry MCP server is hosted by Henry, so there is nothing to install. Point your MCP client or agent at one URL: ``` https://mcp.henrylabs.ai/mcp ``` It exposes the Henry Shopping API as one MCP tool per operation — product search, product details, carts, checkout, orders, and merchants — and ships interactive product and cart views for clients that support [MCP Apps](#interactive-views). You can authenticate two ways: * **API key** — send your Henry API key as a header. Best for agents, scripts, CI, and anything that runs without a person at a browser. * **Sign in** — connect with no key and your client opens a browser window to sign in with your Henry account. Best for chat apps like Claude and ChatGPT. ## Endpoints | URL | Use it for | | - | - | | `https://mcp.henrylabs.ai/mcp` | Claude, Cursor, VS Code, Codex, agents, and other general MCP clients. Every tool, including purchase. | | `https://mcp.henrylabs.ai/mcp/apps` | ChatGPT apps. Discovery and cart tools only; checkout hands off to Henry's hosted checkout page. | ## Authentication ### API key Send a Henry API key from the [Henry Dashboard](https://app.henrylabs.ai) (your app's Developer settings) in either header: ```http theme={null} x-api-key: sk_test_... ``` ```http theme={null} Authorization: Bearer sk_test_... ``` The key works on both endpoints and doesn't expire until you revoke it in the dashboard. Start with a sandbox (`sk_test_`) key; switch to a production (`sk_live_`) key at go-live. Treat the key like a password. Load it from an environment variable or your client's secret prompt — don't commit it to a shared config file. ### Sign in Connect without a key and the server asks your client to sign in with OAuth 2.1: 1. Your client opens a browser window. Sign in to the [Henry Dashboard](https://app.henrylabs.ai) if you aren't already. 2. Approve the connection. To use a specific API key instead of your account, choose **Use an API key instead** and paste the key. 3. You're sent back to your client, which is now connected. A sign-in currently lasts **7 days**; after that, your client asks you to sign in again. Each endpoint has its own sign-in. Either way, tool calls run as the team that owns the API key or account and count toward that team's usage. ## Setup Instructions The examples read your key from `HENRY_SDK_API_KEY`: ```sh theme={null} export HENRY_SDK_API_KEY="your_api_key" ``` ### Claude Code ```sh theme={null} claude mcp add --transport http henry https://mcp.henrylabs.ai/mcp \ --header "x-api-key: $HENRY_SDK_API_KEY" ``` To share the server with your team without sharing a key, check a project-scoped `.mcp.json` into the repo root. Claude Code expands the variable from each developer's environment, and falls back to sign-in when it's unset: ```json theme={null} { "mcpServers": { "henry": { "type": "http", "url": "https://mcp.henrylabs.ai/mcp", "headers": { "x-api-key": "${HENRY_SDK_API_KEY:-}" } } } } ``` Run `/mcp` inside a Claude Code session to check the connection, or to sign in if you didn't set a key. ### Claude (web and desktop) 1. Open **Settings > Connectors** and choose **Add custom connector**. 2. Name it `Henry` and enter `https://mcp.henrylabs.ai/mcp` as the URL. 3. Choose **Connect** and complete the sign-in. ### ChatGPT 1. Enable developer mode under **Settings > Apps & Connectors > Advanced**. 2. Create a connector with the URL `https://mcp.henrylabs.ai/mcp/apps` and OAuth authentication. 3. Complete the sign-in when prompted. ### Cursor Open **Cursor Settings > Tools & MCP > New MCP Server** and add: ```json theme={null} { "mcpServers": { "henry": { "url": "https://mcp.henrylabs.ai/mcp", "headers": { "x-api-key": "${env:HENRY_SDK_API_KEY}" } } } } ``` ### Visual Studio Code Run **MCP: Open User Configuration** from the Command Palette and add the server. VS Code prompts for the key once and stores it securely: ```json theme={null} { "inputs": [ { "type": "promptString", "id": "henry-api-key", "description": "Henry API key", "password": true } ], "servers": { "henry": { "type": "http", "url": "https://mcp.henrylabs.ai/mcp", "headers": { "x-api-key": "${input:henry-api-key}" } } } } ``` ### Codex Add the server to `~/.codex/config.toml`. Codex sends the key as a bearer token: ```toml theme={null} [mcp_servers.henry] url = "https://mcp.henrylabs.ai/mcp" bearer_token_env_var = "HENRY_SDK_API_KEY" ``` ### Windsurf Under **Cascade > MCP Servers**, choose **Add custom server** and add: ```json theme={null} { "mcpServers": { "henry": { "serverUrl": "https://mcp.henrylabs.ai/mcp", "headers": { "x-api-key": "${env:HENRY_SDK_API_KEY}" } } } } ``` ### Others Any client that supports remote MCP servers over **Streamable HTTP** can connect to `https://mcp.henrylabs.ai/mcp`, sending the key as `x-api-key` or `Authorization: Bearer`. Clients that support OAuth can connect without a key and sign in instead. ## Headless agents API keys make the server usable from agents that run unattended. ### Claude Agent SDK ```typescript theme={null} import { query } from "@anthropic-ai/claude-agent-sdk"; for await (const message of query({ prompt: "Find white sneakers under $100 and build a cart", options: { mcpServers: { henry: { type: "http", url: "https://mcp.henrylabs.ai/mcp", headers: { "x-api-key": process.env.HENRY_SDK_API_KEY ?? "" }, }, }, }, })) { if (message.type === "result") console.log(message); } ``` ### Claude API (MCP connector) The Messages API can call the server directly. `authorization_token` is sent as `Authorization: Bearer`: ```typescript theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, betas: ["mcp-client-2025-11-20"], mcp_servers: [ { type: "url", url: "https://mcp.henrylabs.ai/mcp", name: "henry", authorization_token: process.env.HENRY_SDK_API_KEY, }, ], tools: [{ type: "mcp_toolset", mcp_server_name: "henry" }], messages: [ { role: "user", content: "Find white sneakers under $100 on Henry" }, ], }); ``` ## Available Tools | Group | Tools | | - | - | | Products | `productSearch`, `productSearchStatus`, `productDetails`, `productDetailsStatus` | | Carts | `cartCreate`, `cartFetch`, `cartList`, `cartAddItem`, `cartUpdateItem`, `cartRemoveItem`, `cartDelete` | | Checkout | `cartDetails`, `cartDetailsStatus`, `cartPurchase`, `cartPurchaseStatus` | | Orders | `ordersList` | | Merchants | `merchantsList`, `merchantsRetrieve`, `merchantsSearch` | The `/mcp/apps` endpoint omits `cartPurchase`, `cartPurchaseStatus`, `merchantsRetrieve`, and `merchantsSearch`. Card tokenization is never exposed over MCP — agents buy with a `cardToken` your backend creates, or hand off to hosted checkout via `checkoutUrl`. Every tool declares `readOnlyHint`, `destructiveHint`, and `openWorldHint` annotations, so clients can auto-approve read-only calls and confirm the rest. ### Async tools Product search, product details, checkout details, and purchase run as background jobs. The first call returns a `refId` and a `processing` status; the agent then calls the matching `*Status` tool with that `refId` until the status is `complete`. See [Polling](/v1/sdk/server/guides/polling) for how the underlying jobs behave. | Start | Poll | | - | - | | `productSearch` | `productSearchStatus` | | `productDetails` | `productDetailsStatus` | | `cartDetails` | `cartDetailsStatus` | | `cartPurchase` | `cartPurchaseStatus` | ## Interactive views In clients that support [MCP Apps](https://modelcontextprotocol.io/extensions/apps) (such as Claude and ChatGPT), product search, product details, and cart tools can render an interactive view alongside the result: a product carousel, a product detail sheet, and a cart with a link to Henry's hosted checkout. Clients without MCP Apps support receive the same data as a normal tool result.