Stripe Checkout subscription shipping: charge and ship every renewal

Stripe Checkout shows shipping options only in payment mode, so a subscription session cannot offer shipping rates. The usual fix charges shipping as its own recurring price on the subscription, taxed with Stripe's Shipping tax code, and packs each renewal when an invoice.paid event arrives. Address changes, skips, and rate changes stay in your code.
Verified against official documentationReviewed Send a correction (opens in a new tab)

Stripe's shipping guide states the limit in one line: "Only Checkout Sessions in payment mode support shipping options." Its guide to dynamic shipping options repeats it: "Shipping rates aren't available in subscription mode." A subscription box, a coffee club, or a refill plan still has to charge for postage on every cycle, so the shipping charge moves somewhere Stripe does support in subscription mode: a line item. Every subscription then renews as an invoice, and the box behind it is yours to pack, address, and track.

If you would rather each renewal arrive as an order with its own shipping quote and fulfillment, that is how Flint subscriptions work.

Where to start

Where you areYour path
Every renewal ships for the same flat rateShipping as a recurring price
Shipping depends on the address, the weight, or carrier ratesQuote shipping on each renewal
You need a reliable signal to pack each boxShip each renewal from invoice.paid
Subscribers move, ask to skip a month, or your rates changeBetween renewals
A box shipped twice, to the wrong address, or not at allThe six traps
You would rather each renewal arrive as a shipped orderRenewals as orders, on Flint

Flat rate: shipping as a recurring price

When every box ships for the same amount, shipping is one more recurring price on the subscription. Stripe's tax docs describe this exact setup: "To charge tax on shipping for subscriptions, you can create a Product or pass product_data for a line item called "shipping" and select the shipping tax_code."

Shell
# A Product for shipping, carrying Stripe Tax's Shipping tax code
curl https://api.stripe.com/v1/products \
  -u "sk_test_...:" \
  -d name=Shipping \
  -d tax_code=txcd_92010001

# A recurring $5.00 Price on the same interval as the box
curl https://api.stripe.com/v1/prices \
  -u "sk_test_...:" \
  -d product=prod_... \
  -d unit_amount=500 \
  -d currency=usd \
  -d tax_behavior=exclusive \
  -d "recurring[interval]=month"

Then sell the box and the shipping together in subscription mode:

Shell
# line_items[0] is the monthly box, line_items[1] the monthly shipping price
curl https://api.stripe.com/v1/checkout/sessions \
  -u "sk_test_...:" \
  -d mode=subscription \
  -d "line_items[0][price]=price_..." \
  -d "line_items[0][quantity]=1" \
  -d "line_items[1][price]=price_..." \
  -d "line_items[1][quantity]=1" \
  -d "shipping_address_collection[allowed_countries][0]=US" \
  -d "automatic_tax[enabled]=true" \
  --data-urlencode "success_url=https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}"

A subscription with several prices produces one invoice per billing period that combines them, paid in one charge, so the buyer sees the box and shipping on one receipt. Both prices must share a currency, and the shipping price should share the box's interval so each invoice carries exactly one shipment's postage.

Address collection is a separate parameter from shipping options. Stripe documents the payment mode limit for shipping options, and the API reference lists no mode limit for shipping_address_collection. When Checkout creates the Customer, it saves the shipping information it collected there. When you pass an existing customer, add customer_update[shipping]=auto so the new address replaces the old one; the default is never. The Customer's shipping is what renewal invoices copy and what Stripe Tax reads first for subscription invoices.

Variable rates: quote shipping on each renewal

A recurring price is a fixed amount. When postage depends on the address, the weight, or a carrier's rate, the first box and each renewal need their own quote. For the first box, collect the address on your own page, quote it, and pass shipping as a one-time price. In subscription mode, Stripe bills one-time prices on the initial invoice only:

JavaScript
// Your page collected the address before Checkout, because a
// subscription session can't quote shipping. The Customer already has
// that address as its shipping, which Stripe Tax reads.
const firstShipping = await quoteShipping(customerAddress, cart); // your rates

const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  customer: customerId,
  line_items: [
    { price: boxPriceId, quantity: 1 },
    {
      // One-time price: subscription mode bills it on the first invoice only
      quantity: 1,
      price_data: {
        currency: "usd",
        unit_amount: firstShipping,
        tax_behavior: "exclusive",
        product: process.env.STRIPE_SHIPPING_PRODUCT_ID, // tax code txcd_92010001
      },
    },
  ],
  automatic_tax: { enabled: true },
  success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
});

For renewals, use the draft window. Stripe creates each renewal invoice, sends invoice.created, and leaves the invoice in draft for about an hour before it finalizes and charges. An invoice item created with the invoice parameter during that hour lands on that renewal:

JavaScript
// Webhook handler: acknowledge invoice.created at once and quote later.
// Renewal invoices stay drafts for about an hour.
if (event.type === "invoice.created") {
  const invoice = event.data.object;
  if (invoice.billing_reason === "subscription_cycle") {
    await jobs.enqueue("quote-renewal-shipping", { invoiceId: invoice.id });
  }
  return res.sendStatus(200);
}

// Background job
const invoice = await stripe.invoices.retrieve(invoiceId);
if (invoice.status !== "draft") return alertMissedShipping(invoice);

const customer = await stripe.customers.retrieve(invoice.customer);
const amount = await quoteShipping(customer.shipping.address, invoice.lines.data);

await stripe.invoiceItems.create(
  {
    customer: invoice.customer,
    invoice: invoice.id, // without it, the item waits for the next invoice
    amount,
    currency: invoice.currency,
    description: "Shipping",
    tax_code: "txcd_92010001",
    tax_behavior: "exclusive",
  },
  { idempotencyKey: `renewal-shipping-${invoice.id}` },
);

The hour is the constraint. An item created after finalization goes to the next invoice, and an endpoint that fails to acknowledge invoice.created holds up finalization of every automatically collected invoice on the account for up to 72 hours, except invoices with a custom scheduled finalization time. Quote outside the request, and treat a renewal that finalizes without a shipping line as an alert.

Ship each renewal from invoice.paid

Stripe generates an invoice for each billing period; the shipment is yours to create. Stripe's subscription webhook guide pairs invoice.paid with provisioning, and the invoice's billing_reason says why it exists: subscription_create for the signup, subscription_cycle for a renewal, subscription_update for a change. Give every box exactly one trigger. For the first box that trigger is the session: with delayed payment methods such as ACH Direct Debit, the session completes with payment_status unpaid and checkout.session.async_payment_succeeded follows when the money arrives, so the handler listens for both:

JavaScript
switch (event.type) {
  case "checkout.session.completed":
  // Delayed methods such as ACH Direct Debit complete the session unpaid,
  // then send this event when the payment succeeds.
  case "checkout.session.async_payment_succeeded": {
    const session = event.data.object;
    if (session.mode !== "subscription" || session.payment_status === "unpaid") break;

    // The first box, shipped once per session
    await shipOnce(`session:${session.id}`, {
      subscription: session.subscription,
      shipTo: session.collected_information.shipping_details,
    });
    break;
  }

  case "invoice.paid": {
    const invoice = event.data.object;
    // Renewals only. subscription_create is the first box, shipped above;
    // subscription_update is a plan change, not a delivery.
    if (invoice.billing_reason !== "subscription_cycle") break;

    await shipOnce(`invoice:${invoice.id}`, {
      // API 2025-03-31.basil and later; older versions use invoice.subscription
      subscription: invoice.parent.subscription_details.subscription,
      // The address as it stood when this invoice finalized
      shipTo: invoice.customer_shipping,
    });
    break;
  }
}

shipOnce is your own table with a unique key, written in the same transaction as the shipment record. Stripe's webhook docs warn that an endpoint "might occasionally receive the same event more than once," and keying on the session or invoice ID also covers the cases where Stripe sends two separate events for one object, and the session that both checkout events describe. If your page collected the address before Checkout, as in the variable rate flow, the session has no collected_information.shipping_details: ship the first box to the Customer's shipping instead. A renewal whose payment fails sends no invoice.paid until a retry succeeds, so a box can go out days after its renewal date. The webhook events reference covers ordering and duplicate delivery in more depth.

Between renewals: moves, skips, and rate changes

Address changes

The customer portal can edit the Customer's shipping address when shipping is in its allowed updates:

cURL
curl https://api.stripe.com/v1/billing_portal/configurations \
  -u "sk_test_...:" \
  -d "features[customer_update][enabled]=true" \
  -d "features[customer_update][allowed_updates][]=shipping" \
  -d "features[customer_update][allowed_updates][]=email"

A change applies to the next renewal invoice that has not finalized. Until finalization, invoice.customer_shipping mirrors the Customer's shipping; after it, the invoice keeps the address it had, and its tax was calculated there. A subscriber who moves in the gap between payment and packing needs a decision from you, which the traps cover. For subscriptions that charge automatically, Stripe also sends invoice.upcoming a few days before each renewal, with the lead time set in your Dashboard, which is the natural moment to email the subscriber the address their next box is going to.

Skipping a month

Stripe has no skip action. The usual stand-in is pause_collection: the subscription stays active, invoices keep being created, and with behavior set to void they are voided. While collection is paused, Stripe sends no upcoming invoice emails or webhooks for those invoices, so no invoice.paid fires and nothing ships:

Shell
# Skip the November 1 renewal: void invoices created until November 2
curl https://api.stripe.com/v1/subscriptions/sub_... \
  -u "sk_test_...:" \
  -d "pause_collection[behavior]=void" \
  -d "pause_collection[resumes_at]=1793577600"

resumes_at has to fall after the renewal being skipped and before the next one, and Stripe does not send customer.subscription.paused for a collection pause, so record the skip in your own system for support and reporting.

Stripe also has a full pause, POST /v1/subscriptions/{id}/pause, for subscriptions in flexible billing mode. It takes effect immediately, ends the current period at the pause time, and stops invoice generation until you resume, and resuming can start a new billing period with an invoice right away. That suits an open-ended hold. For skipping one box while later boxes keep their dates, pause_collection is the closer fit.

Changing the shipping rate

A Stripe price's amount can't change after you create it. A new carrier rate means a new price and an update to every subscription that carries the old one:

Shell
# 1. A new shipping Price; an existing price's amount can't change
curl https://api.stripe.com/v1/prices \
  -u "sk_test_...:" \
  -d product=prod_... \
  -d unit_amount=700 \
  -d currency=usd \
  -d tax_behavior=exclusive \
  -d "recurring[interval]=month"

# 2. For each subscription, point its shipping item at the new price
curl https://api.stripe.com/v1/subscriptions/sub_... \
  -u "sk_test_...:" \
  -d "items[0][id]=si_..." \
  -d "items[0][price]=price_..." \
  -d proration_behavior=none

proration_behavior defaults to create_prorations, so without none the switch creates proration items for the rest of the current period. Existing subscriptions keep the old price until you move them.

The six traps

  1. The first box ships twice#

    New subscribers receive two boxes in their first week.

    The signup payment is reported twice: checkout.session.completed for the session, and invoice.paid for the subscription's first invoice, whose billing_reason is subscription_create. A handler on each event ships a box for each.

    Confirm it

    Count shipments per subscription for subscribers who signed up last month. Any subscription with two shipments before its first renewal date has both handlers live.

    Fix

    Give each box exactly one trigger. Ship the first box from checkout.session.completed, or from checkout.session.async_payment_succeeded when a delayed payment method such as ACH Direct Debit completes the session unpaid, and renewals from invoice.paid with billing_reason set to subscription_cycle, and record each shipment against the session or invoice ID so a redelivered event changes nothing.

  2. A plan change ships an extra box#

    A subscriber upgraded mid-month and received a box the same day.

    invoice.paid fires for every paid invoice on the subscription, not only renewals. An update with proration_behavior set to always_invoice invoices the proration immediately, and that invoice arrives with billing_reason set to subscription_update.

    Confirm it

    Group last quarter's shipments by the billing_reason of the invoice that triggered them. Anything other than subscription_cycle is a box nobody scheduled.

    Fix

    Filter on billing_reason in the invoice.paid handler and ship only subscription_cycle. Treat subscription_update as a billing event with no shipment.

  3. The box goes to the old address#

    A subscriber updated their address in the portal, and the renewal still shipped to the old one.

    A renewal invoice is a draft for about an hour, then finalizes and charges. Until finalization, invoice.customer_shipping mirrors customer.shipping; after it, the invoice keeps the address it had, and the tax on that invoice was calculated for that address. A move recorded after finalization reaches the next renewal, not this one.

    Confirm it

    When you pack, compare invoice.customer_shipping with the Customer's current shipping. A mismatch is a subscriber who moved after paying.

    Fix

    Read both at packing time and decide by policy: ship to the new address when the tax and shipping would not change, and contact the buyer or refund otherwise. Tell subscribers in the portal when a change applies to the next box.

  4. Two subscriptions share one shipping address#

    A buyer added a gift subscription for a relative, and their own renewal shipped to the relative.

    shipping is one field on the Customer. The customer portal edits that field, renewal invoices copy it, and Stripe Tax uses it first when it locates the customer for subscription invoices. Two recipients cannot both live there.

    Confirm it

    List customers with more than one active subscription that ships. Each one is a single address standing in for several.

    Fix

    Create a separate Customer for each recipient's subscription, so each has its own shipping address and tax location. Keeping a second address in metadata ships to the right door but leaves tax calculated for the wrong one.

  5. A skip charges anyway, or skips two boxes#

    A subscriber asked to skip one month and was billed, or missed two boxes.

    Stripe has no skip action. The usual stand-in is pause_collection with behavior set to void and a resumes_at time: invoices created while collection is paused are voided, and no invoice.paid fires for them. If resumes_at lands before the skipped renewal's invoice is created, that renewal is charged. If it lands after the following renewal, both are voided.

    Confirm it

    For each skip, confirm exactly one voided invoice sits between the pause and its resumes_at.

    Fix

    Set resumes_at after the renewal being skipped and well before the next one, such as one day after the skipped renewal date, computed from the subscription's billing dates rather than today's date.

  6. Quoted shipping misses the invoice#

    Some renewals charged no shipping, and others finalized days late.

    Adding a per-renewal shipping line means creating an invoice item while the invoice is a draft, which lasts about an hour. Two separate failures follow from that. A rate quote that finishes after the invoice finalizes misses it, and the item waits for the next invoice instead. An endpoint that doesn't acknowledge invoice.created, for example because a slow carrier call runs inside the request, makes Stripe delay finalizing every automatically collected invoice for up to 72 hours, except those with a custom scheduled finalization time. That keeps the draft open longer, but it delays every renewal on the account.

    Confirm it

    Search finalized subscription_cycle invoices for ones with no shipping line, and compare invoice.created times with finalization times.

    Fix

    Return 2xx from invoice.created immediately, quote shipping in a background job, and alert when a renewal invoice finalizes without a shipping line.

On Flint, each renewal is a shipped order

Flint is a payments API with commerce built in, and a Flint subscription renews as an order. For a plan that ships, that order carries the subscriber's address, a shipping charge quoted for that renewal, tax calculated for the shipping address, and a fulfillment you ship like any other order. A plan lists the products and the delivery methods subscribers can choose from:

cURL
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: plan-coffee-club-v1" \
  -d '{
    "name": "Coffee club",
    "billing_interval": "monthly",
    "billing_interval_count": 1,
    "currency": "USD",
    "line_items": [
      {"variant_id": "var_1kmn0aExample", "quantity": 2}
    ],
    "quantity_options": [1, 2],
    "inventory_routing_source": {"type": "fixed_location", "location_id": "loc_1kmn0aExample"},
    "subscription_delivery_method_ids": ["dmet_1kmn0aExample", "dmet_1kmn0bExample"]
  }'

Each product needs a delivery profile, as it would for a one-time sale, and the plan is checked when you save it: a product that can't ship, lines that can't ship together, or a method that can't be priced without the buyer present fails with an error that names the line or method. Sign subscribers up through hosted checkout:

cURL
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: checkout-coffee-club-001" \
  -d '{
    "subscription_plan_id": "plan_1kmn0aExample",
    "redirects": {
      "success_redirect_url": "https://example.com/welcome"
    }
  }'

Checkout collects the shipping address, shows the plan's methods with their prices, and charges the first box as the signup order. The recurring price is shown with shipping, such as "$30.00 every month, plus shipping ($5.00 today)" for a method quoted on each renewal. The subscription stores the address, recipient, and method as its delivery.

Three days before a monthly renewal, Flint emits subscription.renewal_upcoming and emails the buyer the items, address, method, and estimated total, with links to skip, change the address, or pause. On the billing date it quotes shipping again with current rates and weights, calculates tax, and charges the saved payment method. If the method can't be quoted for the address, Flint holds the renewal instead of charging for a box it can't route. The paid renewal looks like this, abbreviated:

JSON
{
  "order_id": "ord_1kmn0aExample",
  "origin": "subscription",
  "subscription_id": "sub_1kmn0aExample",
  "subscription_cycle": 4,
  "payment_status": "paid",
  "fulfillment_status": "not_fulfilled",
  "delivery_destination": {
    "recipient": {"name": "Ada Lovelace"},
    "address": {
      "line1": "12 Oak St",
      "city": "Portland",
      "state": "OR",
      "postal_code": "97205",
      "country": "US"
    }
  }
}

subscription_cycle counts charged cycles only, so skipped or held cycles never use a number and 4 is always the fourth shipment. Renewal shipments send the same order.fulfillment.* events as one-time orders, so one fulfillment integration handles both. A skip is one call:

cURL
curl -X POST https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample/skip-cycle \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: skip-sub-ada-001" \
  -d '{"initiated_by": "buyer"}'

A move replaces the subscription's delivery. Flint previews delivery to the new address before saving, so a method that can't serve it fails now rather than at the next renewal:

cURL
curl -X PATCH https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "expected_version": 7,
    "delivery": {
      "type": "shipment",
      "delivery_method_id": "dmet_1kmn0aExample",
      "destination": {
        "type": "address",
        "address": {
          "line1": "48 Birch Ave",
          "city": "Portland",
          "state": "OR",
          "postal_code": "97214",
          "country": "US"
        }
      },
      "recipient": {"name": "Ada Lovelace"}
    }
  }'

If the current renewal is already paid but not shipped, it keeps the address it was paid with, and subscription.delivery_updated carries its open_renewal_order_id so you can decide before packing. When your store settings allow it, buyers skip and change their address themselves, from Flint's buyer account or from your site through a customer session, and change cadence or quantity within what the plan offers.

Stripe and Flint subscription shipping, side by side

Stripe Billing with CheckoutFlint subscriptions
Shipping choice at signupNone in subscription mode; shipping is a line item you addThe plan's delivery methods, with prices, on hosted checkout
Shipping on each renewalA fixed recurring price, or an invoice item your job adds within an hourQuoted again on each renewal, or a fixed method price
Tax on shippingStripe Tax with the Shipping tax code on the line itemCalculated for the shipping address on each renewal order
What a renewal createsAn invoiceAn order with a delivery destination and a fulfillment
Signal to shipinvoice.paid, filtered by billing_reasonorder.fulfillment.created on the renewal order
AddressOne per Customer, shared by all its subscriptionsPer subscription, in delivery
Skip a renewalpause_collection with a hand-set resumes_atskip-cycle, for you or the buyer
Address can't be served at renewalYour own check, such as on invoice.upcoming for subscriptions that charge automaticallyRenewal held before charging, with buyer and merchant emails

Questions, answered

Does Stripe Checkout support shipping options in subscription mode?

No. Stripe's docs say "Only Checkout Sessions in payment mode support shipping options," and the dynamic shipping guide says shipping rates aren't available in subscription mode. Charge shipping as a line item instead: a recurring price for a flat rate, or a one-time price for the first box plus an invoice item on each renewal when the rate varies.

How do I charge shipping on every Stripe subscription renewal?

Create a Product named Shipping with the Shipping tax code, txcd_92010001, give it a recurring Price on the same interval as the subscription, and add it as a second line item. Each billing period produces one invoice that combines the box and the shipping, paid in one charge.

Is shipping taxed on Stripe subscriptions?

With Stripe Tax, a shipping line item is taxed according to the tax code on its product. Stripe's guidance for subscriptions is a line item called shipping with the Shipping tax code, and Stripe Tax decides whether shipping is taxable at the customer's location, which for subscription invoices is the Customer's shipping address when one is set.

Which Stripe webhook should trigger shipping a subscription box?

invoice.paid with billing_reason set to subscription_cycle, for renewals. Ship the first box from checkout.session.completed, or from checkout.session.async_payment_succeeded for delayed payment methods such as ACH Direct Debit, and ignore invoice.paid for subscription_create and subscription_update invoices so that neither the signup nor a plan change ships a second box.

How do subscribers change their shipping address on a Stripe subscription?

Enable the customer portal's customer update feature with shipping in allowed_updates, or update the Customer's shipping field from your own UI. The change reaches the next renewal invoice that hasn't finalized yet. An invoice that already finalized keeps the address, and the tax, it had.

How do I let a subscriber skip a month on Stripe?

Stripe has no skip action. The usual stand-in is pause_collection: set it with behavior void and a resumes_at time after the renewal being skipped and before the next one. The invoice created during the pause is voided, no invoice.paid fires, and nothing ships.

How do I change the shipping price on existing Stripe subscriptions?

A price's amount can't be changed. Create a new shipping Price, then update each subscription's shipping item to it, with proration_behavior set to none if the new rate should start at the next renewal without a mid-period charge or credit.

Does Stripe create an order or shipment for each subscription renewal?

Stripe generates an invoice for each billing period. The shipment, its tracking number, and the address it went to are records your own system keeps, keyed to that invoice.

How does Flint charge shipping on subscriptions?

A Flint subscription plan lists the delivery methods subscribers can choose. Hosted checkout collects the address and shows those methods with their prices, and each renewal becomes an order that quotes shipping again, calculates tax for the shipping address, charges the saved payment method, and gets a fulfillment.

Sources

Accessed October 9, 2026.