# Dynamically customize shipping options

Update shipping options based on a customer's shipping address.

# Embedded form


> Learn more about [the embedded form integration](https://docs.stripe.com/payments/checkout/how-checkout-works.md?payment-ui=checkout-form).

Learn how to dynamically update shipping options based on the address your customer enters in the embedded form.

### Use cases 

- **Show relevant shipping options**: Display only the shipping methods available for the customer’s address. For example, offer overnight shipping only for deliveries within your country.
- **Calculate shipping rates dynamically**: Calculate and display shipping fees based on the customer’s delivery address.
- **Update shipping rates based on the order total**: Offer shipping rates based on the shipping address or order total, such as free shipping for orders over 100 USD.

### Limitations 

- This feature is only supported in [payment mode](https://docs.stripe.com/api/checkout/sessions/object.md#checkout_session_object-mode). [Shipping rates](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-shipping_options) aren’t available in subscription mode.
- This feature doesn’t support the [Express Checkout Element](https://docs.stripe.com/elements/express-checkout-element.md). Wallets such as Apple Pay and Google Pay collect the shipping address directly, bypassing the server-side update flow.
- The embedded form doesn’t support the `permissions` parameter. The form collects shipping details on the client, and your server calculates and sets the available shipping options.
- The embedded form doesn’t display a loading indicator while shipping options update. Calculate shipping options quickly to minimize delays for your customers.

## Create a Checkout Session [Server-side]

From your server, create a *Checkout Session* (A Checkout Session represents your customer's session as they pay for one-time purchases or subscriptions through Checkout. After a successful payment, the Checkout Session contains a reference to the Customer, and either the successful PaymentIntent or an active Subscription) with the following settings:

- Set [ui_mode](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-ui_mode) to `form`.
- Set [shipping_address_collection.allowed_countries](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-shipping_address_collection-allowed_countries) to the countries you ship to.
- Use [shipping_options.shipping_rate_data](https://docs.stripe.com/api/checkout/sessions/create.md#create_checkout_session-shipping_options-shipping_rate_data) to create an initial shipping rate of 0 USD. Your server replaces this rate with the calculated rates after the customer enters their shipping address.

```curl
curl https://api.stripe.com/v1/checkout/sessions \
  -u "<<YOUR_SECRET_KEY>>:" \
  -d ui_mode=form \
  -d "shipping_address_collection[allowed_countries][0]=US" \
  -d "shipping_options[0][shipping_rate_data][display_name]=Shipping rate" \
  -d "shipping_options[0][shipping_rate_data][type]=fixed_amount" \
  -d "shipping_options[0][shipping_rate_data][fixed_amount][amount]=0" \
  -d "shipping_options[0][shipping_rate_data][fixed_amount][currency]=usd" \
  -d "line_items[0][price]={{PRICE_ID}}" \
  -d "line_items[0][quantity]=1" \
  -d mode=payment \
  --data-urlencode "return_url=https://example.com/return"
```

## Customize shipping options [Server-side]

Create an endpoint on your server that calculates shipping options based on the customer’s shipping address.

1. Extract the Checkout Session ID and the customer’s shipping details from the request body.
2. Calculate the available shipping options for the customer’s address.
3. [Update](https://docs.stripe.com/api/checkout/sessions/update.md) the [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md) with the calculated [shipping_options](https://docs.stripe.com/api/checkout/sessions/update.md#update_checkout_session-shipping_options).
4. Return a response to the client indicating whether the update succeeded.

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

app.post("/calculate-shipping-options", async (req, res) => {
  const { checkout_session_id, shipping_details } = req.body;

  // Calculate the shipping options
  const shipping_options = calculateShippingOptions(shipping_details);

  // Update the Checkout Session with the new shipping options
  if (shipping_options !== undefined) {
    const session = await stripe.checkout.sessions.update(checkout_session_id, {
      shipping_options,
    });

    return res.json({ type: "object", value: { succeeded: true } });
  } else {
    return res.json({
      type: "error",
      message: "We can't find shipping options. Please try again.",
    });
  }
});

// Return an array of shipping options or undefined if no options are available
function calculateShippingOptions(shippingDetails) {
  // TODO: Remove error and implement...
  throw new Error(
    "Calculate shipping options based on the customer's shipping details."
  );
}

app.listen(4242, () => {
  console.log('Running on port 4242');
});
```

## Mount the embedded form [Client-side]

#### HTML + JS

The embedded form is available through [Stripe.js](https://docs.stripe.com/js.md). Add the Stripe.js script to the `head` of your HTML file, and create an empty DOM node (container) to mount the form.

```html
<head>
  <script src="https://js.stripe.com/dahlia/stripe.js"></script>
</head>
<body>
  <div id="checkout-form">
    <!-- Embedded form is inserted here -->
  </div>
</body>
```

Initialize Stripe.js with your publishable API key.

```javascript
// Set your publishable key: remember to change this to your live publishable key in production
// See your keys here: https://dashboard.stripe.com/apikeys
const stripe = Stripe('<<YOUR_PUBLISHABLE_KEY>>');
```

Fetch the client secret, create the Checkout instance, mount the form, and load the actions. Listen for the [change](https://docs.stripe.com/js/custom_checkout/element_events/on_change?type=checkoutForm) event to detect when the customer completes their shipping address. Then, use [runServerUpdate](https://docs.stripe.com/js/custom_checkout/run_server_update) to call your server and calculate the shipping options.

After your server updates the Checkout Session, `runServerUpdate` refreshes the Session state to keep the UI in sync. It enforces a 20-second timeout and rejects on failure, so wrap calls in `try`/`catch`.

Track whether the shipping address has been completed to avoid duplicate requests. If the customer edits their address, reset the tracking state so a new calculation runs after they complete the updated address.

```javascript
let hasCompletedShippingAddress = false;

const updateShippingOptions = async (sessionId, shippingDetails) => {
  const response = await fetch("/calculate-shipping-options", {
    method: "POST",
    headers: { "Content-type": "application/json" },
    body: JSON.stringify({
      checkout_session_id: sessionId,
      shipping_details: shippingDetails,
    }),
  });

  const result = await response.json();

  if (result.type === "error") {
    throw new Error(result.message);
  }

  return result;
};

async function initialize() {
  // Fetch the client secret from your server
  const response = await fetch("/create-checkout-session", {
    method: "POST",
  });
  const { clientSecret } = await response.json();

  // Initialize the checkout instance
  const checkout = await stripe.initCheckoutFormSdk({
    clientSecret,
  });

  // Create and mount the embedded form
  const checkoutForm = checkout.createForm();
  checkoutForm.mount('#checkout-form');

  checkoutForm.on("change", async (event) => {
    const { value, status } = event;

    if (status.shippingAddress?.complete && !hasCompletedShippingAddress) {
      hasCompletedShippingAddress = true;

      const loadActionsResult = await checkout.loadActions();
      if (loadActionsResult.type !== 'success') {
        hasCompletedShippingAddress = false;
        return;
      }
      const actions = loadActionsResult.actions;

      try {
        await actions.runServerUpdate(() =>
          updateShippingOptions(actions.getSession().id, value.shippingAddress)
        );
      } catch (error) {
        hasCompletedShippingAddress = false;
      }
    } else if (!status.shippingAddress?.complete && hasCompletedShippingAddress) {
      hasCompletedShippingAddress = false;
    }
  });
}

initialize();
```

#### React

Install [react-stripe-js](https://docs.stripe.com/sdks/stripejs-react.md) and the Stripe.js loader from npm:

```bash
npm install --save @stripe/react-stripe-js @stripe/stripe-js
```

Initialize the `stripe` instance outside of a component’s render to avoid recreating the object on every render. Wrap your checkout page in `CheckoutFormProvider` to provide the form context.

```jsx
import {useMemo} from 'react';
import {loadStripe} from '@stripe/stripe-js';
import {CheckoutFormProvider} from '@stripe/react-stripe-js/checkout';
import CheckoutPage from './CheckoutPage';

const stripePromise = loadStripe('<<YOUR_PUBLISHABLE_KEY>>');

const App = () => {
  const clientSecret = useMemo(() => (
    fetch('/create-checkout-session', {method: 'POST'})
      .then((response) => response.json())
      .then((json) => json.client_secret)
  ), []);

  return (
    <CheckoutFormProvider stripe={stripePromise} options={{clientSecret}}>
      <CheckoutPage />
    </CheckoutFormProvider>
  );
};

export default App;
```

Render the `CheckoutForm` component and pass an [onChange](https://docs.stripe.com/js/custom_checkout/element_events/on_change?type=checkoutForm) callback to detect when the customer completes their shipping address. Then, use [runServerUpdate](https://docs.stripe.com/js/custom_checkout/run_server_update) to call your server and calculate the shipping options.

After your server updates the Checkout Session, `runServerUpdate` refreshes the Session state to keep the UI in sync. It enforces a 20-second timeout and rejects on failure, so wrap calls in `try`/`catch`.

```jsx
import React from 'react';
import {useCheckoutForm, CheckoutForm} from '@stripe/react-stripe-js/checkout';

const CheckoutPage = () => {
  const hasCompletedShippingAddress = React.useRef(false);
  const checkoutState = useCheckoutForm();

  if (checkoutState.type === 'error') {
    return <div>Error: {checkoutState.error.message}</div>;
  }

  const updateShippingOptions = async (sessionId, shippingDetails) => {
    const response = await fetch("/calculate-shipping-options", {
      method: "POST",
      headers: { "Content-type": "application/json" },
      body: JSON.stringify({
        checkout_session_id: sessionId,
        shipping_details: shippingDetails,
      }),
    });

    const result = await response.json();

    if (result.type === "error") {
      throw new Error(result.message);
    }

    return result;
  };

  const handleChange = async (event) => {
    const { value, status } = event;

    if (status.shippingAddress?.complete && !hasCompletedShippingAddress.current) {
      hasCompletedShippingAddress.current = true;

      const loadActionsResult = await checkoutState.checkout.loadActions();
      if (loadActionsResult.type !== 'success') {
        hasCompletedShippingAddress.current = false;
        return;
      }
      const actions = loadActionsResult.actions;

      try {
        await actions.runServerUpdate(() =>
          updateShippingOptions(actions.getSession().id, value.shippingAddress)
        );
      } catch (error) {
        hasCompletedShippingAddress.current = false;
      }
    } else if (!status.shippingAddress?.complete && hasCompletedShippingAddress.current) {
      hasCompletedShippingAddress.current = false;
    }
  };

  return <CheckoutForm onChange={handleChange} />;
};

export default CheckoutPage;
```

## Test the integration

Follow these steps to test your integration and confirm that your custom shipping options work as expected.

1. Create a sandbox environment that mirrors your production setup, and configure it with your Stripe sandbox API keys.

2. Test different shipping addresses to confirm that your `calculateShippingOptions` function handles each scenario.

3. Verify server-side logic by using logging or debugging tools to confirm that your server calculates shipping options and updates the [Checkout Session](https://docs.stripe.com/api/checkout/sessions/object.md) with the new [shipping_options](https://docs.stripe.com/api/checkout/sessions/update.md#update_checkout_session-shipping_options). Confirm the update response includes the new shipping options.

4. Complete the checkout process multiple times in your browser to test the client-side behavior. Pay attention to how the UI updates after entering shipping details. Confirm that:

   - The [change](https://docs.stripe.com/js/custom_checkout/element_events/on_change?type=checkoutForm) event fires when the customer completes their shipping address.
   - The [runServerUpdate](https://docs.stripe.com/js/custom_checkout/run_server_update) call triggers your server endpoint.
   - The shipping options update based on the provided address.
   - An error message appears when shipping is unavailable.

5. Enter invalid shipping addresses and simulate server errors to test client-side and server-side error handling.

