# Sell through agents as a SaaS platform

Learn how to let your businesses sell their products through AI chat agents.

> If your platform wants to use agentic commerce to sell your businesses’ products, [join the waitlist](https://go.stripe.global/agentic-commerce-contact-sales). Agentic commerce for platforms is available to businesses in the US and requires waitlist approval.

Use agentic commerce to let your connected accounts sell products through AI chat agents. Your platform uploads [product feed data](https://docs.stripe.com/agentic-commerce/product-feed.md) and configures checkout hooks for tax and fees, and connected accounts opt in to agent channels. When a buyer makes a purchase through an agent, Stripe runs checkout and calls your platform’s hooks before completing the transaction.

A simplified overview of the agentic commerce flow for platforms (See full diagram at https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents)

```text
[Platform] -- Uploads product catalog data for each connected account --> [Stripe]
[Stripe] -- Distributes feed to enabled agent channels --> [AI agent]
[Buyer] -- Selects product and initiates purchase --> [AI agent]
[AI agent] -- Initiates checkout --> [Stripe]
[Stripe] -- Calls customize_checkout hook --> [Platform]
[Platform] -- Returns customizations --> [Stripe]
[Buyer] -- Completes checkout (shipping, payment) --> [AI agent]
[AI agent] -- Confirms checkout --> [Stripe]
[Stripe] -- Calls finalize_checkout hook (approval, application fee) --> [Platform]
[Platform] -- Approves order with application fee --> [Stripe]
[Stripe] -- Confirms checkout success --> [AI agent]
[AI agent] -- Renders confirmation message to customer --> [Buyer]
[Stripe] -- Sends checkout.session.completed webhook --> [Platform]
[Platform] -- Fulfills order --> [Platform]
```

## Set up your platform

Go to the [Agentic commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard to onboard your platform. The onboarding wizard guides you through:

- Creating a [Stripe Profile](https://docs.stripe.com/get-started/account/profile.md) for your platform.
- Choosing your charge type (*direct* (A charge type where customers transact directly with a connected account, which is always the merchant of record. With each payment, the connected account pays fees to Stripe and, optionally, to your platform) or *destination* (A charge type where customers transact with your platform for products or services provided by a connected account. Your platform pays Stripe fees and immediately transfers funds to the connected account with each payment) with *on\_behalf\_of* (A parameter that allows a Connect platform to create an indirect payment where the connected account is the merchant of record (MoR))).
- Enabling AI agent channels your connected accounts can sell through.
- Configuring seller settings, including your webhook endpoint, receipt type, and support policies.

After you complete onboarding, you can manage these settings from the same page.

## Choose your charge type

In the in-context shopping flow, Stripe creates an agentic checkout session for your platform. To create the session, Stripe needs to know which charge type to use. Stripe supports direct charges and destination charges with the `on_behalf_of` parameter. In both charge types, the connected account acts as the seller.

Configure your charge type during [platform onboarding](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#set-up-platform), or update it later from the [Agentic commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard.

## Upload your product catalog data to Stripe

### Prepare your product catalog

For each connected account, create a CSV file that conforms to the [Stripe product catalog specification](https://docs.stripe.com/agentic-commerce/product-feed.md).

### Upload the product catalog data to Stripe

Upload product catalog data for each connected account separately. Use a sandbox to validate parsing, field mappings, and data quality before enabling live updates.

After you set up catalog uploads, follow these best practices to prevent issues with outdated inventory or pricing. For example, agents might continue to report that a product isn’t available (even if it’s back in stock) until they receive an explicit signal that the inventory has been updated. To troubleshoot continued issues, see [Handle out-of-stock and price failures](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#handle-checkout-failures).

### Keep catalog feeds current

To prevent purchase failures during agentic checkout, refresh each connected account’s catalog data frequently. How frequently depends on how fast the account’s inventory moves, but in most cases, uploading inventory and pricing data every 15 minutes for each connected account is enough. Product data (titles, descriptions, images, and categories) changes less often, so uploading it once per day is usually enough.

Between full uploads, send targeted changes with the [incremental inventory](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#inventory-updates) and [incremental price](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#price-updates) feeds instead of re-uploading an account’s entire catalog.

### Cleanly remove old products

When a connected account removes a product from its inventory, send a clean deletion. In upsert mode, omitting a product from your feed leaves it unchanged in Stripe’s catalog. To explicitly remove it, set `delete=true`. Otherwise, the product remains visible to shoppers and agents.

### Only use replace mode for complete inventory refreshes

Product feed uploads support two processing modes: `upsert` and `replace`. In replace mode, any product not included in the uploaded file is permanently deleted from that connected account’s catalog in Stripe. Use `upsert` for all incremental updates, and `replace` only for intentional full-catalog product refreshes. See [Feed processing mode](https://docs.stripe.com/agentic-commerce/product-feed.md#feed-processing-mode).

| Feed type | Frequency | Purpose |
| --- | --- | --- |
| [Product data](https://docs.stripe.com/agentic-commerce/product-feed.md#product-feed-specification) | Once per day | Titles, descriptions, images, and categories |
| [Inventory](https://docs.stripe.com/agentic-commerce/product-feed.md#inventory-feed-spec) | Every 15 minutes | Prevents agents from showing out-of-stock items |
| [Pricing](https://docs.stripe.com/agentic-commerce/product-feed.md#price-feed-spec) | Every 15 minutes | Helps keep the checkout price aligned with the quoted price |
| [Promotions](https://docs.stripe.com/agentic-commerce/product-feed.md#promotion-feed-spec) | As needed | Offers discount codes, deals, and free shipping to drive conversion |

> Feed uploads are processed as independent, asynchronous tasks. We don’t guarantee processing or completing uploads in the order you submit them. If you upload multiple files in quick succession, a later upload can finish before an earlier one.

For inventory that moves faster than your upload cadence, set up the [product price and availability hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#product-price-availability) so Stripe can confirm current price and stock immediately before a checkout completes.

#### API

> If you use a [restricted API key](https://docs.stripe.com/keys.md#create-restricted-api-key), it must have **Product Catalog Import** write permission. Without this permission, API requests return a `403` error.

Create a `ProductCatalogImport` for the connected account. A successful request returns a [ProductCatalogImport object](https://docs.stripe.com/api/v2/commerce/product-catalog-imports/object.md) in the `awaiting_upload` state. The `status_details.awaiting_upload.upload_url.url` field in the response contains the presigned URL for your file upload. The maximum upload size is 4GB.

```curl
curl -X POST https://api.stripe.com/v2/commerce/product_catalog/imports \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-07-29.preview" \
  -H "Stripe-Account: {{CONNECTEDACCOUNT_ID}}" \
  --json '{
    "feed_type": "product",
    "mode": "upsert",
    "metadata": {
        "file_name": "march_11_2026_product_upload.csv"
    }
  }'
```

```json
{
  "id": "pcimprt_xxx",
  "object": "v2.commerce.product_catalog_import",
  "created": "2026-03-26T00:35:01.000Z",
  "feed_type": "product",
  "status": "awaiting_upload",
  "status_details": {
    "awaiting_upload": {
      "upload_url": {
        "expires_at": "2026-03-26T00:40:02.000Z",
        "url": "https://stripeusercontent.com/files/us-west-2/upload/wksp_xxx"
      }
    }
  },
  "livemode": true
}
```

> The presigned upload URL expires after 5 minutes. Upload your CSV before the URL expires. If it expires, create a new `ProductCatalogImport` to get a new upload URL.

Upload your CSV to the presigned URL. The file must be in CSV format, where each row represents one product or variant. The maximum file size is 4 GB.

```curl
curl -X PUT \
  -H "Content-Type: text/csv" \
  --data-binary @"/path/to/your/file.csv" \
  "{{PRESIGNED_URL}}"
```

After Stripe receives the file, the import transitions from `awaiting_upload` to `processing`.

### Monitor feed status

Stripe validates and cleans the product catalog data, indexes it, and converts it to a format that AI agents can use. We recommend listening for webhook events so you’re notified as soon as indexing completes, instead of polling for status. Listen for the `v2.commerce.product_catalog.imports.succeeded`, `v2.commerce.product_catalog.imports.succeeded_with_errors`, and `v2.commerce.product_catalog.imports.failed` terminal webhook events. The webhook includes the import ID in `related_object.id`, which you can use to retrieve the full object.

If you can’t receive webhook events, poll the [import object](https://docs.stripe.com/api/v2/commerce/product-catalog-imports/object.md) instead until it reaches a terminal state: `succeeded`, `succeeded_with_errors`, or `failed`.

```curl
curl https://api.stripe.com/v2/commerce/product_catalog/imports/{{PRODUCTCATALOGIMPORT_ID}} \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-07-29.preview" \
  -H "Stripe-Account: {{CONNECTEDACCOUNT_ID}}"
```

If the import completes without errors, it reaches `succeeded`. If the file is structurally valid but contains row-level validation issues, the import reaches `succeeded_with_errors`. If the import can’t complete, for example because the file was never uploaded, the file is unreadable, or an internal error occurs, it reaches `failed`.

If your import status is `succeeded_with_errors`, you can download the error file:

1. Find the `status_details.succeeded_with_errors.error_file.url` field in the response.
2. Download the CSV directly from that URL before it expires.
3. The CSV contains only the rows that failed, with a leading `stripe_error_message` column describing each error.

> Error file URLs expire after 5 minutes. To get a new URL, call the [retrieve endpoint](https://docs.stripe.com/api/v2/commerce/product-catalog-imports/retrieve.md) again.

#### Dashboard

Go to the **Connected accounts** page and select the account, then scroll to the **Money Management** section. Under **Agentic commerce**, upload a product catalog CSV for that account. You can view catalog errors and product catalog status from this page.

You can also filter your connected accounts list by **Product catalog status** to see which accounts have catalogs uploaded.

## Configure taxes for your connected accounts

Use Stripe Tax for fully managed tax calculations, or implement a tax hook in your integration if you need custom tax logic or must integrate a third‑party tax provider.

#### Automated tax calculation with Stripe Tax

Use Stripe Tax to calculate and collect tax for each of your connected accounts. See the [Tax for platforms setup](https://docs.stripe.com/tax/tax-for-platforms.md) to configure Stripe Tax.

In your product catalog CSV upload, set the `stripe_product_tax_code` column to associate each product with a tax treatment.

See [tax codes](https://docs.stripe.com/tax/tax-codes.md) for the full list of supported tax codes.

You can also calculate taxes through [third-party tax providers](https://docs.stripe.com/tax/third-party-apps.md), such as Anrok and Avalara.

#### Custom tax calculation with hooks

Use a [checkout customization hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#checkout-customization-hook) to handle the `v1.delegated_checkout.customize_checkout` event and return `tax_rates` for each line item.

To set up the tax hook:

1. Create an endpoint on your server to receive checkout customization events.

See [webhooks](https://docs.stripe.com/webhooks.md) for more information about setting up webhooks in your application.

#### Node.js

```javascript
const stripe = require('stripe');
const express = require('express');
const app = express();

app.use("/checkout-customization-hook", express.raw({ type: "application/json" }));
app.post("/checkout-customization-hook", (req, res) => {
  const sig = req.headers["stripe-signature"];
  const endpointSecret = process.env.WEBHOOK_SECRET; // Store this securely

  try {
    stripe.webhooks.signature.verifyHeader(req.body, sig, endpointSecret, stripe.webhooks.DEFAULT_TOLERANCE);
  } catch (err) {
    console.log(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  const event = JSON.parse(req.body);
  if (event.type === "v1.delegated_checkout.customize_checkout") {
    const data = event.data;
    const connectedAccount = event.context;

    const response = {
      line_items:
        data.line_item_details?.map((item) => ({
          id: item.id,
          tax_rates: [
            {
              rate_data: {
                display_name: "Sales tax",
                inclusive: false,
                percentage: 8.5, // Replace with your actual tax rate
              },
            },
            {
              rate_data: {
                display_name: "Environmental fee",
                inclusive: false,
                percentage: 1.0, // Replace with your actual fee rate
              },
            },
          ],
        })) || [],
    };

    return res.status(200).json(response);
  }

  return res.status(400).json({ error: "Unsupported webhook type" });
});

app.listen(4567, () => console.log("Server is running on port 4567"));
```

1. Register your webhook endpoint URL on the [Agentic commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard.
2. Handle the `v1.delegated_checkout.customize_checkout` event and return tax rates for each line item, as shown in the code example.

## Monetize transactions

By default, when you create an agentic checkout session, no application fee is applied.

> You must add an application fee when your platform pays Stripe fees for a connected account. Without an application fee, you can lose money on each transaction.

#### Application fee hook

Use a [checkout customization hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#checkout-customization-hook) to handle the `v1.delegated_checkout.finalize_checkout` event and return an application fee.

See [webhooks](https://docs.stripe.com/webhooks.md) for information about setting up webhooks in your application.

#### Node.js

```javascript
const stripe = require('stripe');
const express = require('express');
const app = express();

app.use("/agentic-commerce-hook", express.raw({ type: "application/json" }));
app.post("/agentic-commerce-hook", async (req, res) => {
  const sig = req.headers["stripe-signature"];
  const endpointSecret = process.env.WEBHOOK_SECRET; // Store this securely

  try {
    stripe.webhooks.signature.verifyHeader(req.body, sig, endpointSecret, stripe.webhooks.DEFAULT_TOLERANCE);
  } catch (err) {
    console.log(`Webhook signature verification failed: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  const event = JSON.parse(req.body);
  console.log("Received event:", event.type);
  if (event.type === "v1.delegated_checkout.finalize_checkout") {
    const data = event.data;
    // Perform validation checks for order approval
    const orderAmount = data.amount_total || 0;
    const lineItems = data.line_items_details || [];
    const connectedAccount = event.context;
    const stripeCheckoutSessionId = data.checkout_session;

    const applicatonFee = await calculateApplicationFee(lineItems);
    const {reason, isApproved} = await calculateOrderApproval();

    if (isApproved) {
      return res.status(200).json({
        manual_approval_details: {
          type: "approved"
        },
        application_fee_details: {
          application_fee_amount: applicatonFee
        }
      });
    } else {
      return res.status(200).json({
        manual_approval_details: {
          type: "declined",
          declined: {
            reason,
          }
        }
      });
    }
  }

  return res.status(400).json({ error: "Unsupported webhook type" });
});

app.listen(4567, () => console.log("Server is running on port 4567"));
```

#### Charge an application fee with the platform pricing tool

If a connected account uses a fee payer configuration where your platform pays Stripe fees, you can use the [platform pricing tool](https://docs.stripe.com/connect/platform-pricing-tools.md) to charge an application fee.

## Enable your connected accounts to sell with agents

Connected accounts must review the agent terms and enable an agent before they can sell through it. Stripe sends the agent an approval request that the agent must accept. Connected accounts can manage which AI agents sell their products, customize how their business appears across agent platforms, and pause or stop selling on any agent.

#### Embedded component

Embed the agentic commerce settings component directly in your platform’s UI to give connected accounts a native onboarding and management experience. For details on how to embed Dashboard functionality into your website, see [Get started with Connect embedded components](https://docs.stripe.com/connect/get-started-connect-embedded-components.md).

#### Ruby

```ruby
require 'sinatra'
require 'stripe'
# This is a placeholder - it should be replaced with your secret API key.
# Sign in to see your own test API key embedded in code samples.
# Don't submit any personally identifiable information in requests made with this key.
# Don't put any keys in code. See https://docs.stripe.com/keys-best-practices.
client = Stripe::StripeClient.new('<<YOUR_SECRET_KEY>>')

post '/account_session' do
  content_type 'application/json'

  begin
    account_session = client.v1.account_sessions.create({
      account: '{{CONNECTED_ACCOUNT_ID}}',
      components: {
        agentic_commerce_settings: {
          enabled: true,
        }
      }
    })

    {
      client_secret: account_session[:client_secret]
    }.to_json
  rescue => error
    puts "An error occurred when calling the Stripe API to create an account session: #{error.message}";
    return [500, { error: error.message }.to_json]
  end
end
```

#### HTML + JS

```html
<head>
  <script type="module" src="index.js" defer></script>
</head>
<body>
  <h1>Payments</h1>
  <div id="container"></div>
  <div id="error" hidden>Something went wrong!</div>
</body>
```

```javascript
import {loadConnectAndInitialize} from '@stripe/connect-js';

const fetchClientSecret = async () => {
  // Fetch the AccountSession client secret
  const response = await fetch('/account_session', { method: "POST" });
  if (!response.ok) {
    // Handle errors on the client side here
    const {error} = await response.json();
    console.error('An error occurred: ', error);
    document.querySelector('#error').removeAttribute('hidden');
    return undefined;
  } else {
    const {client_secret: clientSecret} = await response.json();
    document.querySelector('#error').setAttribute('hidden', '');
    return clientSecret;
  }
}

const stripeConnectInstance = loadConnectAndInitialize({
    // This is a placeholder - it should be replaced with your publishable API key.
    // Sign in to see your own test API key embedded in code samples.
    // Don't submit any personally identifiable information in requests made with this key.
    publishableKey: "<<YOUR_PUBLISHABLE_KEY>>",
    fetchClientSecret: fetchClientSecret,
  });
const paymentComponent = stripeConnectInstance.create("agentic-commerce-settings");
const container = document.getElementById("container");
container.appendChild(paymentComponent);
```

#### Full Stripe Dashboard

Connected accounts with access to the full Stripe Dashboard can onboard to agentic commerce by going to **Settings > Payments > Agentic commerce**. From this page, they can create a Stripe Profile, configure their seller settings, and enable or disable agent channels.

#### Express Dashboard

Connected accounts using the Express Dashboard can go to **Settings** and find the **Agentic commerce** section. From there, they can manage their seller profile, configure settings, and enable or disable agent channels.

## Respond to purchases and fulfill orders

Listen to Stripe webhooks to monitor orders made on AI chat agents.

When an order is confirmed, Stripe emits webhook events that your server can handle to run fulfillment logic. Set up an endpoint on your server to accept, process, and acknowledge these events. See the [webhooks guide](https://docs.stripe.com/webhooks.md) for step-by-step instructions on integrating with and testing Stripe webhooks.

Stripe emits `checkout.session.completed` and `payment_intent.succeeded`. If your fulfillment logic already handles these events, you don’t need additional integration changes. You can customize your fulfillment logic for in-context agentic selling (for example, by noting in your order confirmation email that the checkout occurred through an agent).

For details on setting up webhooks for connected accounts, see [Connect webhooks](https://docs.stripe.com/connect/webhooks.md).

#### Node.js

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();

// Use the secret provided by Stripe CLI for local testing
// or your webhook endpoint's secret
const endpointSecret = 'whsec_...';

app.post('/webhook', async (request, response) => {
  const sig = request.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
  } catch (err) {
    response.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  if (event.type === 'checkout.session.completed') {
    let session = event.data.object;
    // For V1 webhooks event.account is the connected account
    session = await stripe.checkout.sessions.retrieve(
      session.id,
      {
        expand: ["line_items.data.price.product", "line_items.data.price"],
      },
      // If using direct charges, the checkout session is created on the connected account and the `stripeAccount` parameter must be passed. If using destination charges the checkout session is created on the platform account and `stripeAccount` parameter can be ignored.
      {
        stripeAccount: "{{CONNECTED_ACCOUNT_ID}}",
      },
    );

    // SKU id is available at session.line_items.data[number].price.external_reference
    fulfillCheckout(event.account, session);
  }

  response.status(200).send();
});
```

After you receive the webhook, retrieve all required fields with a single API call. To avoid multiple requests, expand sub-resources using the [expand](https://docs.stripe.com/api/expanding_objects.md) request parameter with the preview header `Stripe-Version: 2025-12-15.preview`.

```curl
curl https://api.stripe.com/v1/checkout/sessions/{{SESSION_ID}}?expand[]=line_items.data.price.product&expand[]=line_items.data.taxes&expand[]=payment_intent.latest_charge \
  -u <<YOUR_SECRET_KEY>>: \
  -H "Stripe-Version: 2025-12-15.preview" \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}"
```

See fields in the expanded [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md), such as `amount_total`, quantity, and SKU ID.

### Checkout session field reference

| Order field | Available resource | API path |
| --- | --- | --- |
| **Order date** | `CheckoutSession.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.created` |
| **Order quantity** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].quantity` |
| **SKU** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].price.external_reference` |
| **Product description** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].price.product.description` |
| **Unit price** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].price.unit_amount` |
| **Tax amount** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].Taxes[].amount` |
| **Tax type** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].Taxes[].Rate.tax_type` |
| **Tax rate** | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].Taxes[].Rate.percentage` |
| **ShippingAddress** | `CheckoutSessions` | `CheckoutSessions.CollectedInformation.shipping_details` |
| **BillingAddress** | `CheckoutSessions.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.billing_details` |
| **Last4** | `CheckoutSessions.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.payment_method_details.card.last4` |
| **ExpMonth** | `CheckoutSessions.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.payment_method_details.card.exp_month` |
| **ExpYear** | `CheckoutSessions.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.payment_method_details.card.exp_year` |
| **CreditCardType** | `CheckoutSessions.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.payment_method_details.card.brand` |
| **Final amount** | `CheckoutSessions` | `CheckoutSessions.amount_total` |
| **GTIN** (Private preview) | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].price.product.identifiers.gtin` |
| **MPN** (Private preview) | `CheckoutSessions` | `CheckoutSession.LineItems.Data[].price.product.identifiers.mpn` |
| **Agent details** (Private preview) | `CheckoutSession.PaymentIntent` | `CheckoutSessions.PaymentIntent.agent_details` |
| **OrderNo** | `CheckoutSession.PaymentIntent.LatestCharge` | `CheckoutSessions.PaymentIntent.LatestCharge.receipt_number` |

## Test your integration

You can test your integration directly from the Dashboard in a [sandbox](https://docs.stripe.com/sandboxes.md):

1. Open the [Trigger Agentic Purchase](https://dashboard.stripe.com/test/workbench/blueprints/agentic-commerce-turnkey-agentic-purchase) blueprint in Workbench
2. Enter the connected account’s ID and a SKU ID
3. Click **Run** to simulate a charge from an agent

## Optional: Manual capture

By default, Stripe captures payments immediately when a purchase completes. To use manual capture, enable it on the [Agentic commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard. After you enable manual capture, call the capture method on the `PaymentIntent` returned in the webhook described in the previous section.

```curl
curl -X POST https://api.stripe.com/v1/payment_intents/{{PAYMENTINTENT_ID}}/capture \
  -u "<<YOUR_SECRET_KEY>>:"
```

## Optional: Set up an order approval hook

Before we confirm a payment, we check inventory based on your product catalog data and might run fraud checks depending on your [Radar](https://docs.stripe.com/radar.md) setup. To control whether we complete a purchase, configure an order approval hook. Before we complete the checkout flow, we send an approval request to your service. You must approve or decline the request.

Stripe enforces a four-second timeout for your hook. If your hook doesn’t respond within this time, Stripe declines the payment.

To set up an order approval hook:

1. Specify the endpoint on the [Agentic Commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard.
2. Enable **Order approvals**.
3. Implement your logic at the endpoint and use the request and response formats below.

> Stripe returns a `424` status code to agents if your endpoint returns a non-`2xx` status code. Some agents might retry these API requests, so you must make sure that your endpoint is idempotent.

### Request format 

Stripe sends the following request to your endpoint:

```typescript
{
  type: "v1.delegated_checkout.finalize_checkout",
  id: string,
  livemode: boolean,
  // account ID
  context?: string,
  // request specific data
  data: {
    amount_subtotal?: number,
    amount_total?: number,
    billing_details?: {
      name?: string,
      address?: {
        line1?: string,
        line2?: string,
        city?: string,
        state?: string,
        postal_code?: string,
        country?: string
      }
    },
    currency: string,
    email?: string,
    line_item_details: Array<{
      id: string,
      sku_id: string,
      unit_amount: number,
      amount_discount: number,
      amount_subtotal: number,
      amount_tax: number,
      amount_total: number,
      quantity: number,
      name: string,
      tax_rates: Array<{
        rate: {
          id: string,
          display_name: string,
          percentage: number,
          inclusive: boolean,
        },
        // Amount of tax applied for this rate.
        amount: number
      }>
    }>,
    payment_method_details?: {
      type: "card" | ...,
      card?: {
        brand: "amex" | "visa" | "master_card" | ...,
        country?: string,
        exp_month: number,
        exp_year: number,
        fingerprint?: string,
        funding: "credit" | "debit" | "prepaid" | "unknown",
        iin?: string,
        last4: string,
        wallet?: {
          type: "apple_pay" | "google_pay" | ...
        }
      }
    },
    phone?: string,
    shipping_details?: {
      name?: string,
      address?: {
        line1?: string,
        line2?: string,
        city?: string,
        state?: string,
        postal_code?: string,
        country?: string
      },
    },
    total_details?: {
      amount_discount?: number,
      amount_shipping?: number,
      amount_tax?: number
    },
    metadata?: Map<string,string>
  }
}
```

### Response format 

Your endpoint must return `200` HTTP responses with the following format:

```typescript
{
  manual_approval_details: {
    type: "approved" | "declined",
    declined?: {
      reason: string
    }
  },
  // Connect only: set an application fee for the transaction
  application_fee_details?: {
    application_fee_amount: number,
    transfer_data?: {
      amount?: number,
    }
  },
  // will be propagated to Checkout Session metadata
  metadata?: Map<string,string>
}
```

## Optional: Set up a checkout customization hook

By default, Stripe calculates taxes and shipping options for your products based on the options defined in your [product catalog](https://docs.stripe.com/agentic-commerce/product-feed.md).

To calculate taxes or shipping options and costs dynamically with your logic:

1. Specify the endpoint on the [Agentic Commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard.
2. Enable **Custom tax rates** or **Custom shipping options**.
3. Implement your logic at the endpoint and use the request and response formats below.

> Stripe returns a `424` status code to agents if your endpoint returns a non-`2xx` status code. Some agents might retry these API requests, so you must make sure that your endpoint is idempotent.

### Request format 

Stripe sends the following request to your endpoint:

```typescript
{
  type: "v1.delegated_checkout.customize_checkout",
  id: string,
  livemode: boolean,
  // Connected account ID
  context?: string,
  // Request specific data
  data: {
    // Used by the seller to determine whether they can set manual tax rates on line items
    automatic_tax: {
      enabled: boolean,
    },
    currency: string,
    line_item_details?: Array<{
      id: string,
      sku_id: string,
      unit_amount: number,
      amount_discount: number,
      amount_subtotal: number,
      amount_tax: number,
      amount_total: number,
      quantity: number,
      name: string,
      tax_rates: Array<{
        rate: {
          id: string,
          display_name: string,
          percentage: number,
          inclusive: boolean,
        },
        // Amount of tax applied for this rate.
        amount: number
      }>
    }>,
    shipping_details?: {
      // Same as the shipping rate object described at https://docs.stripe.com/api/shipping_rates/object#shipping_rate_object
      shipping_rate?: {
        id: string,
        display_name?: string,
        metadata?: Map<string,string>,
        tax_code?: string ,
        tax_behavior: 'unspecified' | 'inclusive' | 'exclusive',
        fixed_amount: {
          amount: number,
          currency: 'usd' | 'cad' | etc.,
          currency_options: {
            <currency>: {
              amount: number,
              tax_behavior: 'unspecified' | 'inclusive' | 'exclusive',
            }
          }
        },
        delivery_estimate?: {
          maximum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number },
          minimum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number }
        }
      },
      // Same as the shipping rate object described at https://docs.stripe.com/api/shipping_rates/object#shipping_rate_object
      shipping_rates?: Array<{
        id: string,
        display_name?: string,
        metadata?: Map<string,string>,
        tax_code?: string,
        tax_behavior: 'unspecified' | 'inclusive' | 'exclusive',
        fixed_amount: {
          amount: number,
          currency: 'usd' | 'cad' | etc.,
          currency_options: {
            <currency>: {
              amount: number,
              tax_behavior: 'unspecified' | 'inclusive' | 'exclusive',
            }
          }
        },
        delivery_estimate?: {
          maximum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number },
          minimum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number }
        }
      }>,
      address?: {
        line1?: string,
        line2?: string,
        city?: string,
        state?: string,
        postal_code?: string,
        country?: string
      }
    },
    amount_total?: number,
    amount_subtotal?: number,
    total_details?: {
      amount_discount?: number,
      amount_shipping?: number,
      amount_tax?: number
    },
    metadata?: Map<string,string>
  }
}
```

### Response format 

Your endpoint must return a `200` HTTP response with the following format:

```typescript
{
  shipping_options?: Array<{
    // ID of the shipping rate, or data provided to create the shipping rate. Only provide one; not both
    shipping_rate?: string,
    shipping_rate_data: {
      display_name?: string,
      fixed_amount: {
        amount: number,
        currency: 'usd' | 'cad' | etc.,
      },
      metadata?: Map<string,string>,
      tax_code?: string ,
      tax_behavior?: 'unspecified' | 'inclusive' | 'exclusive',
      // Same as the shipping rate object described at https://docs.stripe.com/api/shipping_rates/create#create_shipping_rate-delivery_estimate
      delivery_estimate?: {
        maximum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number },
        minimum: { unit: 'business_day' | 'day' | 'hour' | 'month' | 'year', value: number }
      }
    },
  }>,
  line_items?: Array<{
    // Corresponding ID of the line item to update
    id: string,
    // List of tax rates to apply to this line item
    // Provide either `rate` or `rate_data`
    tax_rates: Array<{
      // ID of a v1 tax rate
      rate?: string,
      // Or `rate_data`.
      // This will use an existing tax rate that matches the params or will create one if a matching rate does not exist
      rate_data?: {
        display_name: string,
        inclusive: boolean,
        // percentage out of 100
        percentage: number,
      }
    },
  }>,
  // will be propagated to Checkout Session metadata
  metadata?: Map<string,string>
}
```

## Optional: Set up a product price and availability hook

Implement this hook to provide agents with real-time pricing and inventory data. Without it, agents rely on your uploaded product catalog, which can result in out-of-stock errors or price mismatches at checkout.

Agents can call this endpoint when inventory is low or before showing a product to a buyer. Stripe can also call the endpoint to keep catalog data current.

To set up a product price and availability hook:

1. Specify the endpoint on the [Agentic Commerce settings](https://dashboard.stripe.com/settings/connect/agentic-commerce) page in the Dashboard.
2. Enable **Product price and availability**.
3. Implement your logic at the endpoint and use the request and response formats below.

### Request format 

Stripe sends the following request to your endpoint:

```typescript
{
  type: "delegated_commerce.product_price_availability",
  id: string,
  livemode: boolean,
  // The ID of the merchant whose data is requested
  context?: string,
  // request specific data
  data: {
    sku_id: string,
  }
}
```

### Response format 

Your endpoint must return a `200` HTTP response with the following format:

```typescript
{
  sku_id: string,
  merchant_id: string,  // must match the context field from the request
  deleted?: boolean,    // when true, the product has been deleted; omit availability and price
  availability?: {      // required unless deleted is true
    status: "in_stock" | "out_of_stock" | "preorder" | "backorder",
    quantity?: number,          // current inventory count
    availability_date?: string  // ISO 8601 date when a preorder item becomes available
  },
  price?: {             // required unless availability.status is "out_of_stock" or deleted is true
    unit_amount: number,        // current selling price in smallest currency unit
    currency: string            // ISO 4217 currency code
  },
  sale_price?: {
    unit_amount: number,        // sale price in smallest currency unit
    currency: string,           // ISO 4217 currency code
    start_date: string,         // ISO 8601 sale start date
    end_date: string            // ISO 8601 sale end date
  },
  as_of?: number                // Unix timestamp of when this data was last accurate
}
```

## Optional: Test your hooks

You can test your order approval or checkout customization hook by providing a publicly accessible endpoint that can accept hook requests with a `POST` method. Set up your endpoint function so that it:

1. Handles `POST` requests with a JSON payload
2. Returns a successful status code (`200`)

For local development, use a tunneling tool such as [ngrok](https://ngrok.com/) to expose your local endpoint.

### Example endpoint

This code snippet shows an endpoint function that receives `v1.delegated_checkout.finalize_checkout` requests and returns a `200` response. You can find the signing secret under the **Agentic Commerce Extension** event destination in your webhook settings in the [Developer Dashboard](https://docs.stripe.com/development/dashboard.md).

#### Ruby

```ruby
require 'json'
require 'sinatra'
require 'stripe'

set :port, 4242

# Replace with your endpoint's secret from your webhook settings
endpoint_secret = 'whsec_...'

# Using Sinatra
post '/hooks' do
  payload = request.body.read
  sig_header = request.env['HTTP_STRIPE_SIGNATURE']

  # Verify the webhook signature
  begin
    Stripe::Webhook::Signature.verify_header(
      payload, sig_header, endpoint_secret
    )
  rescue Stripe::SignatureVerificationError => e
    status 400
    return
  end

  # Handle the payload
  payload = JSON.parse(payload)
  case payload['type']
  when 'v1.delegated_checkout.finalize_checkout'
    # Check inventory and accept payment
    data = { manual_approval_details: { type: 'approved' } }
  end

  status 200
  body data.to_json
end
```

## Optional: Handle the agreement with agents

> This API is in private preview. Examples below are subject to change.

When a connected account onboards to an agent, Stripe creates an Agreement object between the seller and the agent. Stripe emits webhook events to the platform and the agent whenever the Agreement object’s status changes:

- `v2.orchestrated_commerce.agreement.created`: An Agreement was created between the seller and the agent.
- `v2.orchestrated_commerce.agreement.partially_confirmed`: The seller has confirmed the Agreement.
- `v2.orchestrated_commerce.agreement.confirmed`: The agent has confirmed the Agreement.
- `v2.orchestrated_commerce.agreement.terminated`: Either the seller or agent terminated the Agreement.

Listen for these events on your webhook endpoint to keep your platform’s state in sync (for example, updating UI, enabling or disabling agent access, or notifying sellers).

### Fetch an Agreement

Retrieve an Agreement object using the Agreements API:

```curl
curl https://api.stripe.com/v2/orchestrated_commerce/agreements/{{AGREEMENT_ID}} \
  -H "Authorization: Bearer {{API_KEY}}" \
  -H "Stripe-Version: 2025-12-19-17.internal" \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}"
```

## Optional: Send incremental inventory updates

In addition to uploading your product catalog, you can send individual product inventory updates through the [Imports API](https://docs.stripe.com/api/v2/commerce/product-catalog-imports.md). Use the same upload process as catalog uploads, but set `feed_type` to `inventory`. Inventory feeds support only `upsert` mode.

```curl
curl -X POST https://api.stripe.com/v2/commerce/product_catalog/imports \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-07-29.preview" \
  -H "Stripe-Account: {{CONNECTEDACCOUNT_ID}}" \
  --json '{
    "feed_type": "inventory",
    "mode": "upsert",
    "metadata": {
        "file_name": "march_11_2026_inventory_upload.csv"
    }
  }'
```

## Optional: Send incremental price updates

Upload your product catalog data, then send individual price updates through the [Imports API](https://docs.stripe.com/api/v2/commerce/product-catalog-imports.md). Use the same upload process as catalog uploads and set `feed_type` to `pricing`. Price feeds support only `upsert` mode.

```curl
curl -X POST https://api.stripe.com/v2/commerce/product_catalog/imports \
  -H "Authorization: Bearer <<YOUR_SECRET_KEY>>" \
  -H "Stripe-Version: 2026-07-29.preview" \
  -H "Stripe-Account: {{CONNECTEDACCOUNT_ID}}" \
  --json '{
    "feed_type": "pricing",
    "mode": "upsert",
    "metadata": {
        "file_name": "march_11_2026_pricing_upload.csv"
    }
  }'
```

Price mismatches, where the price shown to a shopper doesn’t match the price we charge, cause checkouts to fail. To keep the checkout price aligned with the quoted price:

- Upload pricing feeds as soon as a connected account’s prices change, not on a fixed schedule.
- `sale_price_effective_date` is required when you set a `sale_price`. Stripe uses the end date to stop applying the sale price automatically, with no feed update needed.
- If the [product price and availability hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#product-price-availability) returns a different `price` or `sale_price` than your feed, the hook value takes precedence.

## Optional: Handle out-of-stock and price failures

For each connected account, try to keep the rate at which shoppers attempt to buy a product and are told it’s unavailable at checkout below 5%. The right approach depends on how fast the account’s inventory moves, and you can combine approaches:

- **More frequent feed refreshes**: For most connected accounts with relatively stable inventory, keeping catalog data current on the [recommended cadence](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#upload-catalog) is enough to stay within the threshold.
- **Incremental inventory and price feeds**: Send targeted [inventory](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#inventory-updates) and [price](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#price-updates) updates between full catalog uploads so agents get fresh data without re-uploading an entire catalog.
- **Real-time availability check**: Set up the [product price and availability hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#product-price-availability), an endpoint Stripe calls immediately before a checkout completes to return the current price and confirm that the item is still in stock. It operates independently of your feed refresh frequency, and is particularly valuable if a connected account’s inventory moves fast (such as flash sales or limited drops) or sells on multiple channels where a single item can be purchased elsewhere between feed refreshes.

When the product price and availability hook is enabled, Stripe:

- Calls your endpoint on behalf of agents to query product availability
- Manages request rates so your systems aren’t overwhelmed
- Proactively calls the hook for items showing low inventory, so availability data stays current between feed refreshes

> Your product price and availability hook must respond within 4 seconds. A timeout on this hook causes Stripe to fall back to your feed data and continue the checkout. This differs from the [approval hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#approval-hook) behavior, where a timeout causes Stripe to decline the checkout.

To apply other checks or platform-specific logic before a checkout completes, see the [order approval hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#approval-hook) and the [checkout customization hook](https://docs.stripe.com/connect/saas/tasks/enable-in-context-selling-on-ai-agents.md#checkout-customization-hook).

## Optional: Add promotions to drive conversion

AI agents actively offer promotions to shoppers. Connected accounts with active promotions consistently see higher conversion rates than equivalent products without them.

Use the promotion feed to upload discount codes, percentage-off deals, fixed-amount discounts, and free shipping offers for a connected account. You can target promotions at specific products or an account’s entire catalog, and set automatic application at checkout or require a redemption code. The promotion feed references products by `id` or `item_group_id` from the account’s catalog. Promotions that don’t match active product IDs are ignored.

Upload promotions for each connected account separately through the [Imports API](https://docs.stripe.com/api/v2/commerce/product-catalog-imports.md), using the same upload process as catalog uploads but with `feed_type` set to `promotion`. Promotion feeds support only `upsert` mode. See the [promotion feed specification](https://docs.stripe.com/agentic-commerce/product-feed.md#promotion-feed-spec) for the required fields.

## Optional: Handle refunds and disputes

If you already use the [Payment Intents API](https://docs.stripe.com/api/payment_intents.md) or [Checkout Sessions API](https://docs.stripe.com/api/checkout/sessions.md), your existing refund and dispute flows work without changes for in-context agentic selling. Stripe creates `PaymentIntents` for agentic checkout flows, so as long as you associate the `PaymentIntent` ID with your order, your integration continues to work.

If a customer cancels the order from your website or through customer service after checkout succeeds, you can initiate a refund. Manage refunds and disputes in the Dashboard from the [Transactions page](https://dashboard.stripe.com/payments), or use the [Refunds API](https://docs.stripe.com/api/refunds.md) to handle them programmatically.

## Optional: Export your catalog feed

Use the [Commerce report](https://docs.stripe.com/reports/report-types/commerce.md) to export a snapshot of the catalog feed you imported into Stripe for a connected account. Use the export to perform bulk edits and re-import your catalog, or to verify your import matches Stripe’s records and perform programmatic reconciliation.

Catalog imports can take up to 15 minutes to appear in the export.

### Check data freshness

Retrieve the `catalog_feed.product.1` report type to see when data was last updated:

```curl
curl https://api.stripe.com/v1/reporting/report_types/catalog_feed.product.1 \
  -u <<YOUR_SECRET_KEY>>: \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}"
```

The `data_available_end` field in the response indicates the timestamp of the most recently available data.

### Create a report run

Create a report run to export your catalog feed:

```curl
curl https://api.stripe.com/v1/reporting/report_runs \
  -u <<YOUR_SECRET_KEY>>: \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}" \
  -d "report_type=catalog_feed.product.1"
```

### Poll for completion

The run starts with `status: "pending"`. Poll the report run until the `status` is either `succeeded` or `failed`:

```curl
curl https://api.stripe.com/v1/reporting/report_runs/{{REPORT_RUN_ID}} \
  -u <<YOUR_SECRET_KEY>>: \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}"
```

When the run succeeds, download the CSV file using the URL in `result.url`:

```curl
curl https://files.stripe.com/v1/files/{{FILE_ID}}/contents \
  -u <<YOUR_SECRET_KEY>>: \
  -H "Stripe-Account: {{CONNECTED_ACCOUNT_ID}}"
```
