# Set up hooks

Configure hooks for order approval, checkout customization, and product price and availability.

## 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/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/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/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
```
