Multiple Flat Rate
0.6.0, 0.7.0, 0.7.1, 0.7.2, 1.0, 1.1
snippets, shipping, advance

Multiple Custom Flat Rate Shipping Options

Versions 0.7.1 and older: Note that applying javascript shipping modifications using either live or flat rates where you are setting custom values has been found to not work as expected for subscription based products where you need the shipping to apply to each subscription renewal. A fix is in place for versions 0.7.2 and newer.

Using version 2.0? There is a new snippet available for our latest version, available from here.

If you're using handling fees note that you can't overwrite handling fees using javascript on the checkout to provide free shipping. While it will appear that the shipping is $0 on the checkout, the handling fees will be added back in server-side, and the customer will see a shipping fee that matches the handling fee cost you have set in the administration.

The following functionality allows you to add and update any number of custom flat rate shipping options based on any criteria (eg: what categories are products in, total cost of the cart, shipping destination country). This script is limited to flat rate shipping only.

The functionality described on this page require advanced javascript knowledge, and are not officially supported. They are included in our official wiki because certain shipping methods and functionality are not natively supported, and though we are working on radically improving our shipping functionality, in the meantime these methods may be great workarounds. Use with caution, test, and post in our forum if you run into problems.

See the changelog for details on updates to this script.

Step 1: Update Categories

Update all categories to 'Shipped using a flat rate fee' with a value of 0 in the 'Product Delivery Option' section.

Step 2: Add HTML

Add the following right after the ^^checkout^^ placeholder in your checkout template:

<div id="fc_custom_shipping_methods_container">

Step 3: Add Javascript

Add the following right before the closing </head> tag in your checkout template (we will add code to the “customShippingLogic” function later):

<script type="text/javascript" charset="utf-8">
  FC.checkout.config.customShipping = {
    onLoad: true,  // Set to false if you don't want shipping calculated when the checkout loads
    onLocationChange: false, // Set to true if your shipping logic relies on updating whenever the shipping location for the order changes
    onPreSubmit: true // Set to false if you don't want to load shipping if it hasn't already loaded before the user tries to checkout
  function customShippingLogic() {
    // ... add your custom logic here
<script type="text/javascript" charset="utf-8">
  /* Multiple Flat Rate Shipping Options Logic v2.4 */
  jQuery(document).ready(function() {
    jQuery("#fc_custom_shipping_methods_container").on('click', 'input[name=shipping_service]', function(){
      shipping_service_description = jQuery(this).siblings(".fc_shipping_carrier").html();
      shipping_service_description += ((shipping_service_description == "") ? '' : ' ');
      shipping_service_description += jQuery(this).siblings(".fc_shipping_service").html();
      // Launch FoxyCart functionality
    if (FC.checkout.config.customShipping.onLoad) {
    if (FC.checkout.config.customShipping.onLocationChange) {
      isValidateAndSubmit = false;
      FC.checkout.overload("updateTaxes", function() { if (!isValidateAndSubmit) { runShippingLogic(); } }, null);
      FC.checkout.overload("validateAndSubmit", function() { isValidateAndSubmit = true; }, function() { isValidateAndSubmit = false });
    if (FC.checkout.config.customShipping.onPreSubmit) {
      FC.checkout.overload("validateAndSubmit", function() {if (!jQuery("#shipping_service_id").length) { runShippingLogic(); }}, null);
  function runShippingLogic() {
    // Check to see if there are actually shippable products in the current cart before running the custom shipping (0.7.1+ only), or just run it for older carts
    if ((typeof(FC.checkout.config.hasShippableProducts) === "boolean" && FC.checkout.config.hasShippableProducts) || typeof(FC.checkout.config.hasShippableProducts) === "undefined") {
  // example: addShippingOption(1, 4.99, 'PostBox', 'Express Local');
  function addShippingOption(code, cost, carrier, service) {
    if (jQuery("#fc_shipping_methods_inner").length == 0) {
    carrier = (typeof(carrier) == 'undefined' || carrier == null) ? "" : carrier;
    service = (typeof(service) == 'undefined' || service == null) ? "" : service;
    var newShippingOption = '<label for="shipping_service_' + code + '" class="fc_radio"><input type="radio" class="fc_radio fc_required" value="' + code + '|' + cost + '" id="shipping_service_' + code + '" name="shipping_service" /><span class="fc_shipping_carrier">' + carrier + '</span><span class="fc_shipping_service">' + service + '</span><span class="fc_shipping_cost">' + FC.formatter.currency(cost, true) + '</span></label>';
  // example: updateShippingOptionCost(1, 4);
  function updateShippingOptionCost(code, cost) {
    jQuery("input#shipping_service_" + code).val(code + '|' + cost).siblings("span.fc_shipping_cost").html(FC.formatter.currency(cost, true));
  // example: removeShippingOption(1);
  function removeShippingOption(code) {
    jQuery("label[for=shipping_service_" + code + "]").remove();
    if (jQuery("#fc_shipping_methods_inner").html() == "") {
  function addCustomShippingContainer() {
jQuery("#fc_custom_shipping_methods_container").html('<h2>Shipping Options</h2><div class="fc_row fc_shipping_methods_container" id="fc_shipping_methods_container"><div class="fc_radio_group_container fc_row fc_shipping_methods" id="fc_shipping_methods"><input type="hidden" value="0" id="shipping_service_id" name="shipping_service_id"><input type="text" style="display:none;" value="" id="shipping_service_description" name="shipping_service_description"><input type="text" value="" id="shipping_details" name="Shipping_Details" style="display:none;" /><div class="fc_shipping_methods_inner" id="fc_shipping_methods_inner"></div><label style="display: none;" class="fc_error" for="fc_shipping_methods">Please select a shipping method.</label></div></div>');
  function removeCustomShippingContainer() {

Step 4: Set Update Options

Based on the type of shipping options you're providing, you may need to customise when the shipping options are run in your script. At the top of the script, you need to edit the options array to match the type of functionality you're looking for. Note: If all options are set to false, the shipping options will never run.

FC.checkout.config.customShipping = {
  onLoad: true,
  onLocationChange: false,
  onPreSubmit: true
  • onLoad: If set to true, your shipping logic will run once the checkout has completed loading. Recommended value: true
  • onLocationChange: If set to true, this will run your shipping logic whenever the checkout tries to recalculate taxes - which is generally after the customer changes a location input like country, state and postcode. If your shipping logic is based off of an address detail, this should be set to true. Note that this means your shipping logic may be run multiple times before the customer checks out, so make sure you account for it by using the removeCustomShippingContainer() or updateShippingOptionCost() methods. If not, you may end up in a situation with duplicate copies of the same shipping option.
  • onPreSubmit: If set to true, your shipping logic will run when the user tries to complete the purchase, and if no shipping options have been presented to the customer yet, it loads them at that point. Recommended value: true

Step 5: Customise Shipping Options

Now the fun part, based on whatever criteria you want, add in the different shipping options you require for your site. Add your custom code between the /* BEGIN CUSTOM SHIPPING LOGIC */ and /* END CUSTOM SHIPPING LOGIC */ lines in the first script block. There are four functions available to you.

Description: Adds a shipping option to the checkout.
Parameters: code, cost, carrier, service
Example: addShippingOption(1, 4.99, 'PostBox', 'Local Delivery');
Notes: You don't have to provide both the carrier and the service parameters - but at least one of them is required.
Description: Updates the cost of a existing shipping option.
Parameters: code, cost
Example: updateShippingOptionCost(1, 5.50);
Description: Removes an existing shipping option.
Parameters: code
Example: removeShippingOption(1);
Description: Removes all existing shipping options.
Parameters: none
Example: removeCustomShippingContainer();

Note that the code parameter must be a number. Setting it to a string will result in your checkout failing in a situation where the gateway returns your customer to the checkout with an error.


Example 1
  1. 3 default shipping options: standard and priority with one postal provider, and express with another.
  2. If there are more than 5 products, remove the express option.
  3. If the total weight of the cart is greater that 10, adjust the shipping costs.
addShippingOption(1, 5, 'Postmaster', 'Standard Delivery');
addShippingOption(2, 9.45, 'Postmaster', 'Priority Delivery');
addShippingOption(3, 10, 'PostPlus', 'Express (Next Day)');
if (fc_json.total_weight > 10) {
  updateShippingOptionCost(1, 6);
  updateShippingOptionCost(2, 10);
  updateShippingOptionCost(3, 11.99);
if (fc_json.product_count > 5) {
Example 2
  1. Postage is calculated as a base price per product, with each subsequent product adding an additional cost.
  2. Two different groups of shipping options are presented, one for local delivery within the US, and one for international addresses based off of the shipping country.
  3. Shipping methods are first displayed after the customer has entered address details like country, state and postcode, and reset whenever they change the shipping address to be a different country.
  4. Requires onLocationChange being set to true
if (typeof(country_code) === "undefined") {country_code = "";}
new_country_code = (jQuery("#use_different_addresses").is(":checked") ? $("#shipping_country").val() : $("#customer_country").val());
if (country_code != new_country_code) { // The shipping country has changed!
  country_code = new_country_code;
  removeCustomShippingContainer(); // This call will make sure that when it updates, it starts fresh.
  if (country_code == "US") {
    postage = 10 + ((fc_json.product_count - 1) * 0.50);
    addShippingOption(1, postage, 'USPS', 'Standard');
    postage = 12 + ((fc_json.product_count - 1) * 1.50);
    addShippingOption(2, postage, 'USPS', 'Express');
  } else {
    postage = 15 + ((fc_json.product_count - 1) * 2);
    addShippingOption(3, postage, 'USPS', 'International');
Example 3
  1. Postage is assigned per category.
  2. If there is a product from CategoryA in the cart, then present express option
  3. If there is only a product from CategoryB in the cart, provide free shipping as an option
var hasCategoryA = false;
var hasCategoryB = false;
for (p in fc_json.products) {
  switch (fc_json.products[p
].category) {
    case "CategoryA":
      hasCategoryA = true;
    case "CategoryB":
      hasCategoryB = true;
if (hasCategoryB && !hasCategoryA) {
  addShippingOption(1, 0, '', 'Free Ground Shipping');
} else if (hasCategoryA) {
  addShippingOption(2, 5.99, 'USPS', 'Express')


  • 2011/05/08 - v1.0 - Initial version
  • 2011/06/01 - v1.1 - Fix bug with shipping option id's breaking checkout on gateway error
  • 2011/08/03 - v1.1 - Update example for shipping based on country location
  • 2011/09/09 - v2.0 - Updated to be more robust in preventing customers checking out with no shipping option selected
  • 2012/03/07 - v2.1 - Added a check to make sure that the cart actually has shippable products for newer versions of FoxyCart, to respect the “No Shipping” admin setting
  • 2012/04/12 - v2.2 - Added checks in the addShippingOption() function to account for undefined carriers and services
  • 2012/07/13 - v2.3 - Added checks in onLocationChange updateTaxes overload to not run if it is being run as part of validateAndSubmit to prevent checkout being blocked.
  • 2013/05/18 - v2.4 - Updated script to use .on() instead of .live()

Site Tools