Add shipping to a checkout

A flat shipping rate is one price for every order, such as $9 to anywhere in the US. You set it up once as a delivery method, and Flint's hosted checkout offers it to every buyer whose order has something to ship. The checkout page collects the buyer's shipping address, adds the shipping charge and the tax on it to the total, and saves the address on the order for you to ship to.

For rates that change by region, free shipping over a threshold, in-store pickup, or local delivery, see Delivery options. To price shipping from a carrier API on your own server, see Live delivery rates with a callback.

Before you start#

You need:

  • A test API key with commerce.delivery.write, settings.write, and checkouts.checkout_sessions.write. Reading the order after payment needs commerce.orders.read.
  • An active Location to ship from. Locations you create through the API already have the address and time zone shipping needs.
  • An order with physical items. Every physical product gets a delivery profile that says how it can be delivered. Unless you assign your own, Flint gives it the General profile, which allows shipping. Digital products, services, and gift cards never need shipping, so checkout doesn't ask for a shipping address when an order has only those.

How shipping works at checkout#

Shipping at hosted checkoutResponse
Your backendFlintBuyer1. create a shipping method (once)2. make it the checkout default (once)3. create a checkout session for an ordercheckout_session.urlenter a shipping addressshipping price, tax, and new totalpay4. order.paid webhook, then read the order
  1. Your backend sends 1. create a shipping method (once) to Flint
  2. Your backend sends 2. make it the checkout default (once) to Flint
  3. Your backend sends 3. create a checkout session for an order to Flint
  4. Flint returns checkout_session.url to Your backend
  5. Buyer sends enter a shipping address to Flint
  6. Flint returns shipping price, tax, and new total to Buyer
  7. Buyer sends pay to Flint
  8. Flint sends 4. order.paid webhook, then read the order to Your backend

Steps 1 and 2 are setup. Step 3 is the checkout you already create for each order, unchanged. Step 4 is where your backend gets the shipping address.

1. Create the shipping method#

A delivery method is one choice a buyer can make at checkout. This one ships from your warehouse for $9.00 to any US address:

cURL
curl -X POST https://api.withflintpay.com/v1/delivery-methods \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: method-standard-shipping-001" \
  -d '{
    "name": "Standard shipping",
    "type": "shipment",
    "status": "active",
    "configuration": {
      "origin": {
        "type": "fixed_location",
        "location_id": "loc_1kmn0aExample"
      },
      "eligibility": {
        "country": {"values": ["US"]}
      },
      "pricing": {
        "type": "fixed",
        "fixed": {
          "currency_options": {
            "USD": {"amount": 900, "currency": "USD"}
          }
        }
      },
      "estimate": {
        "type": "transit_time",
        "transit_time": {
          "handling_days": {"minimum": 1, "maximum": 1},
          "transit_days": {"minimum": 2, "maximum": 5}
        }
      }
    }
  }'
  • name is the label the buyer sees next to the price.
  • origin is the Location the order ships from.
  • eligibility limits the method to US addresses. Without it, the method accepts an address in any country.
  • pricing is the rate. Amounts are integers in the currency's minor unit, so 900 is $9.00. The key in currency_options must match the currency inside it. A rate of 0 shows as "Free".
  • estimate gives the buyer an arrival date. Flint counts handling_days and then transit_days as business days in the origin Location's time zone, skipping weekends but not holidays, and checkout shows the result, for example "Arrives Oct 6-9". Leave estimate out to show no date.
  • "status": "active" makes the method usable right away. Activation fails if the origin Location isn't active. Without status, the method is created inactive, and you activate it later with a PATCH whose body is only {"status": "active"}.

Flint responds with 201 Created:

Response
{
  "data": {
    "delivery_method_id": "dmet_1kmn0aExample",
    "current_delivery_method_revision_id": "dmetr_1kmn0aExample",
    "type": "shipment",
    "name": "Standard shipping",
    "status": "active",
    "version": 1,
    "configuration": {
      "origin": {"type": "fixed_location", "location_id": "loc_1kmn0aExample"},
      "eligibility": {"country": {"values": ["US"]}},
      "pricing": {"type": "fixed", "fixed": {"currency_options": {"USD": {"amount": 900, "currency": "USD"}}}},
      "estimate": {
        "type": "transit_time",
        "transit_time": {
          "handling_days": {"minimum": 1, "maximum": 1},
          "transit_days": {"minimum": 2, "maximum": 5}
        }
      }
    },
    "created_at": "2026-10-05T17:04:05Z",
    "updated_at": "2026-10-05T17:04:05Z"
  },
  "request_id": "req_1kmn0aExample"
}

Save delivery_method_id. You need version only when you change the method later.

Tax on the shipping charge#

The shipping charge is taxable by default, and automatic tax charges it at the generally taxable rate where the order ships. To change that for every delivery method, set tax.default_delivery_taxable in your settings. To change it for one method, set configuration.taxable on the method. Sales tax covers how the shipping address becomes the order's tax location.

2. Offer the method on every checkout#

Save the method in your checkout settings, so every checkout offers it without naming it:

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "checkout": {
      "default_delivery_method_ids": ["dmet_1kmn0aExample"]
    }
  }'

default_delivery_method_ids holds up to 25 methods, and each PATCH replaces the whole list. To add a second method later, send both IDs. Send [] to remove the default. Every method in the list, and every zone, location set, or rate callback it uses, must exist and be active. Otherwise saving returns 400 FULFILLMENT_METHOD_UNAVAILABLE.

Each kind of checkout picks its methods this way:

CheckoutMethods it offers
Checkout session created without delivery_method_idsThe default, when the order has items to ship.
Checkout session created with delivery_method_idsOnly the methods listed. [] offers none.
Payment link without its own delivery_method_idsThe default, read each time a buyer opens the link, when the order has items to ship.
Payment link with its own delivery_method_idsOnly the link's methods.
Invoice checkoutThe default, when the invoiced order has items to ship.

3. Create the checkout#

Create the checkout session for the order the way you already do. Leave out delivery_method_ids and the session uses the default:

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-order-1042" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "redirects": {
      "success_redirect_url": "https://example.com/thanks"
    }
  }'

The session lists the methods it offers in delivery_method_ids:

Response
{
  "data": {
    "checkout_session": {
      "checkout_session_id": "cs_1kmn0aExample",
      "status": "open",
      "surface": "hosted",
      "order_id": "ord_1kmn0aExample",
      "delivery_method_ids": ["dmet_1kmn0aExample"],
      "delivery_selection_required": true,
      "url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=cklt_v1..."
    },
    "checkout_access": {
      "checkout_auth_token": "ckat_v1..."
    }
  },
  "request_id": "req_1kmn0aExample"
}

delivery_selection_required is true because the order has items to ship and the buyer hasn't chosen how yet. Send the buyer to checkout_session.url exactly as returned, as Checkout sessions describes.

The session pins the method as it is when the session is created. If you change the rate afterward, open sessions keep the old one and new sessions get the new one.

If the session's methods can't ship one of the order's items, Flint doesn't create the session and returns 400 FULFILLMENT_METHOD_ASSIGNMENT_UNSATISFIABLE. That happens when the default is empty, or when an item's delivery profile doesn't allow shipment.

What the buyer sees#

The checkout page has a Shipping section with an address form: full name, street address, an optional apartment or unit, city, state, and ZIP code, plus a country list when the method ships to more than one country. Address suggestions appear as the buyer types unless you set customer_collection.enable_address_autocomplete to false.

With one method, checkout chooses it for the buyer. It shows the name, the arrival estimate, and the price, then adds a Shipping line to the order summary and updates the tax and total once the address is in. The buyer can't pay until the address is complete. If the address is outside the method's eligibility, checkout says it doesn't ship there and asks for a different address.

To save the buyer some typing, send an address in customer_collection.prefilled_customer_info.shipping_address, or create the checkout for a customer with a saved shipping address. Checkout fills in the form and prices that address as soon as the buyer opens it.

Apple Pay and Google Pay collect the shipping address and show the shipping price in the wallet's payment sheet.

4. Ship the order#

When the buyer pays, Flint sends order.paid. The webhook doesn't include the address, so read the order:

cURL
curl https://api.withflintpay.com/v1/orders/ord_1kmn0aExample \
  -H "Authorization: Bearer YOUR_API_KEY"

The shipping details are in delivery_destination and charges:

Response
{
  "data": {
    "order_id": "ord_1kmn0aExample",
    "payment_status": "paid",
    "delivery_destination": {
      "recipient": {"name": "Ada Lovelace"},
      "address": {
        "line1": "120 Kent Avenue",
        "city": "Brooklyn",
        "state": "NY",
        "postal_code": "11249",
        "country": "US"
      },
      "source": "delivery_selection",
      "delivery_selection_id": "dsel_1kmn0aExample",
      "frozen_at": "2026-10-05T17:09:12Z"
    },
    "charges": [
      {
        "order_charge_id": "och_1kmn0aExample",
        "name": "Standard shipping",
        "type": "shipping_fee",
        "applied_money": {"amount": 900, "currency": "USD"},
        "tax_money": {"amount": 80, "currency": "USD"},
        "total_money": {"amount": 980, "currency": "USD"}
      }
    ]
  }
}
  • delivery_destination is where to ship: the recipient's name, and their phone when checkout asked for one, plus the address. frozen_at is set when the payment succeeds, and the destination can't change after that.
  • The shipping_fee charge is what the buyer paid for shipping, with its tax.

Flint also creates a fulfillment for the shipment, in pending, shortly after the order is paid:

  • order.paid
    The buyer paid. Read the order for delivery_destination.
  • order.fulfillment.created
    Flint created the pending fulfillment for the shipment. Add tracking and move it forward from here.

Fulfillment and shipping covers packing, adding tracking, and the shipping emails the buyer receives.

Change the rate later#

Send only the configuration keys you're changing, with the method's current version as expected_version. This raises the rate to $10.00 and keeps the origin, the US-only rule, and the arrival estimate:

cURL
curl -X PATCH https://api.withflintpay.com/v1/delivery-methods/dmet_1kmn0aExample \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "expected_version": 1,
    "configuration": {
      "pricing": {
        "type": "fixed",
        "fixed": {
          "currency_options": {
            "USD": {"amount": 1000, "currency": "USD"}
          }
        }
      }
    }
  }'

Each key you send replaces that key's whole value, and keys you leave out keep their values. Send null to clear an optional key: "eligibility": null removes the US-only rule, so the method ships to every country. If someone changed the method after you read it, Flint returns 409 DELIVERY_RESOURCE_VERSION_CONFLICT with the current_version. Read the method again and retry. The new rate applies to checkouts created after the change.

To stop offering the method, remove it from default_delivery_method_ids and from any payment link that lists it, including inactive links. Then deactivate it with a PATCH whose body is only {"status": "inactive"}. Until then, deactivating or archiving it returns 409 DELIVERY_RESOURCE_HAS_DEPENDENCIES.

Errors#

  • HTTP 400
    The checkout's methods can't deliver one of the order's items. Add a shipping method to the default or to delivery_method_ids, or check that the item's delivery profile allows shipment.
  • HTTP 400
    A method in delivery_method_ids or the default, or a zone, location set, or rate callback it uses, is missing or not active. blocking_resources names the resource. Activate it, or leave the method out.
  • HTTP 400
    The method uses caller_supplied pricing, which only an embedded checkout can use. Use fixed pricing for hosted checkout.
  • HTTP 400
    The same method ID appears twice in delivery_method_ids or the default.
  • HTTP 400
    A checkout or the default can name at most 25 methods.
  • HTTP 409
    The method is still in the checkout default or on a payment link, so it can't be deactivated or archived. blocking_resources lists each payment link by ID, plus an other entry with no ID when the default includes the method. Remove the method from the default with PATCH /v1/settings and from each link with PATCH /v1/payment-links/{payment_link_id}.
  • HTTP 409
    The method changed after you read it. Read it again and send its current version as expected_version.

Errors lists every code with its remediation.

Next steps#

Was this helpful?