Dynamic Shipping and Tax Calculation in the Orders API

    When using Relay to sell products through apps, use callback-based shipping and tax calculation to provide accurate costs.

    When an app creates an order though the Orders API on your behalf, Stripe checks your shipping and tax settings to dynamically, and in real-time, set those costs. Sellers have four choices for both settings.

    Shipping can be set as:

    • free: No additional cost, the default.
    • flat_rate: A flat additional cost, regardless of the items ordered, the quantity, or the customer’s geographic location. You can even opt to waive the shipping cost above a certain order total.
    • callback: Determined on the fly per order.
    • provider: Calculated using a third party shipping provider like EasyPost.

    Tax can be set as:

    • included: No additional cost, the default.
    • percentage: A flat additional cost, as a percent of the order total, before shipping.
    • callback: Determined on the fly per order.
    • provider: Calculated using a third party tax provider like Avalara or TaxJar.

    Of the three, callback is the most flexible and complex, as it requires you to provide an endpoint on your own server that Stripe will reach out to upon order creation to retrieve these costs. The settings are also flexible enough to support different options for shipping and taxes: one could be flat or free, and the other could leverage a callback.

    If you select callback, create a page on your server that computes and returns the appropriate costs on demand. To know how the page should respond, you should first understand the information it will receive.

    Order creation event

    When an order is created, Stripe will issue a POST HTTP request to the specified endpoint with the details of the unpaid, created order. A sample POST body is:

    {
      "order": {
        "id": "or_15iahK2eZvKYlo2CzKGgMVNl",
        "created": 1426898562,
        "object": "order",
        "livemode": false,
        "status": "created",
        "shipping": {
          "name": "Jenny Rosen",
          "address": {
            "line1": "1234 Main street",
            "line2": null,
            "city": "Anytown",
            "state": "CA",
            "postal_code": "123456",
            "country": "US"
          },
          "phone": null,
          "tracking_number": null,
          "carrier": null
        },
        "items": [
          {
            "amount": 1500,
            "currency": "usd",
            "description": "Unisex / M",
            "object": "order_item",
            "parent": {
              "id": "sku_h8UvZvy9JA4QXeuR5Wxt",
              "object": "sku",
              "active": true,
              "attributes": {},
              "created": 1445530555,
              "currency": "usd",
              "image": null,
              "inventory": {
                "quantity": 42,
                "type":" finite",
                "value": null
              },
              "livemode": false,
              "metadata": {},
              "package_dimensions":null,
              "price": 1500,
              "product": "prod_6naDTQsFnjCXUqY9ZEph",
              "updated": 1445530556
            },
            "type": "sku"
          }
        ],
        "amount": 1500,
        "currency": "usd",
        "charge": null
      }
    }

    For example, your page may calculate the tax due based upon the order amount and the customer’s address, both of which are in the received order object. As another example, shipping may be calculated based upon the total package size, total package weight, and the customer’s address. In this case, the page would need to look up the package’s dimensions, either from a previous store (in your database) or through the Stripe API.

    Your endpoint response

    Your endpoint must return a 200 status code, along with a JSON response. The JSON needs a top-level order_update object containing the following properties:

    • An items array which consists of tax OrderItems to be appended to the order
    • A shipping_methods array which contains the shipping options to be presented to the user (as exposed on the Order object)
    • An optional "order_id" if you have an order identifier that is different from the Stripe identifier (which starts with or_)

    An example of a valid response is:

    {
      "order_update": {
        "order_id": "abc-123-abcdefg",
        "items": [
          {
            "parent": null,
            "type": "tax",
            "description": "Sales taxes",
            "amount": 245,
            "currency": "usd"
          }
        ],
        "shipping_methods": [
          {
            "id": "free_shipping",
            "description": "Free 7-day shipping",
            "amount": 0,
            "currency": "usd",
            // Optional delivery estimate and tax items:
            "delivery_estimate": {
              "type": "exact",
              "date": "2017-12-02"
            },
            "tax_items": []
          }, {
            "id": "priority_shipping",
            "description": "Priority shipping",
            "amount": 499,
            "currency": "usd",
            // Optional delivery estimate and tax items:
            "delivery_estimate": {
              "type": "exact",
              "date": "2017-11-27"
            },
            "tax_items": [
              {
                "parent": "priority_shipping",
                "type": "tax",
                "description": "Shipping sales taxes",
                "amount": 49,
                "currency": "usd"
              }
            ]
          }
        ]
      }
    }

    Note that if your shipping costs are taxable, you must return a set of tax line items for them as well, nested under the "tax_items" property for each method. The one important difference between the top-level tax line items and those that apply to shipping methods is that the "parent" field is set to the "id" of the shipping method. For example, the $0.49 sales tax line item on priority shipping has the parent field "priority_shipping". If no taxes apply to your shipping methods, you can omit the "tax_items" array.

    Indicating an error

    Any non-200 responses will be considered an error, which results in the order creation being rejected. This also means that if your endpoint is broken (e.g., due to a syntax error), orders cannot be created.

    Your endpoint can respond to the Stripe request with error messages that the API will return to the order creation request. Error codes are represented in the usual Stripe format with a type, code, and message:

    {
      "error": {
        "type": "invalid_request_error",
        "code": "shipping_calculation_failed",
        "message": "The shipping information provided could not be verified",
        "param": "shipping.country"
      }
    }

    Acceptable values for code are:

    • sku_inactive: the SKU is marked as inactive
    • product_inactive: the SKU belongs to an inactive product
    • out_of_inventory: this SKU is out of stock
    • maximum_sku_quantity_exceeded: the user tried to order more than the maximum allowed quantity of this SKU.
    • shipping_calculation_failed: the shipping methods could not be computed
    • taxes_calculation_failed: the taxes could not be computed
    • address_verification_failed: the address could not be verified
    • upstream_order_creation_failed: generic error code to use for any other error condition where a specific message should be returned to the end-user

    The different error codes have different requirements for additional data to pass in order to make the user-facing error response as helpful as possible. We’ve outlined the required data below.

    SKU-level errors

    Codes: sku_inactive, product_inactive, out_of_inventory, maximum_sku_quantity_exceeded

    The error response must contain a value for param indicating the index of the order item that is inactive or out of stock. The following is an example of a valid response:

    {
      "error": {
        "type": "invalid_request_error",
        "code": "sku_inactive",
        "message": "The SKU 12345 is no longer active.",
        "param": "items[0]"
      }
    }

    Stripe will translate that to the following API response:

    {
      "error": {
        "type": "invalid_request_error",
        "code": "sku_inactive",
        "message": "This SKU cannot be used to create an order: 12345 is marked as inactive.",
        "param": "items[0]"
      }
    }

    Order-level errors

    Codes: shipping_calculation_failed, taxes_calculation_failed, address_verification_failed

    The error response can contain a value for param indicating which parameter caused the validation to fail. The following is an example of a valid response:

    {
      "error": {
        "type": "action_failed",
        "code": "shipping_calculation_failed",
        "message": "The ZIP code and state do not match.",
        "param": "shipping.address"
      }
    }

    Stripe will translate that to the following API response:

    {
      "error": {
        "type": "action_failed",
        "code": "shipping_calculation_failed",
        "message": "Shipping calculation failed: The ZIP code and state do not match.",
        "param": "shipping.address"
      }
    }

    General errors

    Codes: upstream_order_creation_failed

    The message in the error response will be included in the Stripe API response, but there are no additional requirements for your API response. The response below is a valid response:

    {
      "error": {
        "type": "action_failed",
        "code": "upstream_order_creation_failed",
        "message": "Database connection dropped"
      }
    }

    Stripe will translate that to the following API response:

    {
      "error": {
        "type": "action_failed",
        "code": "upstream_order_creation_failed",
        "message": "Order creation failed: Database connection dropped"
      }
    }

    Next steps

    Congrats! You've gone through how to use callback-based shipping and tax calculations in the Orders API. Some things you might want to see next: