> For the complete documentation index, see [llms.txt](https://docs.datajet-app.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.datajet-app.com/scripts/blank/shipping-rate/shipping-calculator-block.md).

# Shipping Calculator Block

The **Shipping Calculator** is a theme app extension block that lets shoppers preview shipping rates from any storefront page (product detail page or cart page) before they reach checkout. The block reuses your active **Shipping Rate** script — no new script type or backend code is required.

{% hint style="info" %}
This block calls the same `/task/{id}` endpoint that Shopify's Carrier Service uses at checkout, but goes through the app proxy. Your existing shipping\_rate script handles both flows.
{% endhint %}

## How It Works

1. Merchant adds one of two blocks to a theme section in the theme editor:
   * **Shipping Calculator (Product)** — for product detail pages
   * **Shipping Calculator (Cart)** — for the cart page
2. Shopper enters address fields (postal code is required, other fields are toggleable per block).
3. The block sends a `POST` request to `/apps/{proxy-subpath}/task/{script_id}` containing the destination address, line items, currency, locale, and logged-in customer info.
4. Your shipping\_rate script receives the payload as `request.body.rate` (identical shape to the checkout flow) and returns rates.
5. The block parses the response and renders each rate inline.

### Data flow

```
Shopper fills form → Block builds payload → POST /apps/{proxy}/task/{id}
        ↓
DataJet runs your shipping_rate script with request.body.rate
        ↓
Script returns { "rates": [...] }
        ↓
Block renders rates list to shopper
```

## Setup

### Step 1: Activate a Shipping Rate Script

You must have an active **Shipping Rate** script in DataJet before the calculator can return rates. See [Shipping Rate](/scripts/blank/shipping-rate.md) for setup. Note its **script ID** (or **handle**) — you'll paste it into the block settings.

{% hint style="warning" %}
Shopify allows only one active shipping\_rate script per merchant. If you don't have one, you'll get an empty response.
{% endhint %}

### Step 2: Add the Block to Your Theme

1. In Shopify admin, open **Online Store > Themes** and click **Customize** on the active theme.
2. Navigate to a product page (for the Product block) or the cart page (for the Cart block).
3. In a section that accepts blocks, click **Add block** and pick:
   * **Shipping Calculator (Product)** on a product section, or
   * **Shipping Calculator (Cart)** on the cart section.
4. Paste the script ID from Step 1 into the **Shipping rate script ID** field.
5. Configure which address fields to show (see below) and save.

## Block Settings

Both blocks expose the same settings:

| Setting                    | Type     | Default              | Description                                                                         |
| -------------------------- | -------- | -------------------- | ----------------------------------------------------------------------------------- |
| `script_id`                | text     | *required*           | Script ID (Mongo ObjectId) or handle of your active shipping\_rate script.          |
| `title`                    | text     | `Calculate shipping` | Heading rendered above the form. Leave blank to hide.                               |
| `button_label`             | text     | `Calculate`          | Submit button text.                                                                 |
| `show_country`             | checkbox | `true`               | Render a country input.                                                             |
| `show_province`            | checkbox | `false`              | Render a province / state input.                                                    |
| `show_city`                | checkbox | `true`               | Render a city input.                                                                |
| `show_street`              | checkbox | `false`              | Render a street (address1) input.                                                   |
| `show_address2`            | checkbox | `false`              | Render an address line 2 input.                                                     |
| `prefill_customer_address` | checkbox | `true`               | When the shopper is logged in, pre-fill the inputs from `customer.default_address`. |

The postal/ZIP code input is always rendered.

## Request Payload

The block POSTs JSON in the same shape your shipping\_rate script already understands:

```json
{
  "rate": {
    "destination": {
      "country": "US",
      "postal_code": "10001",
      "city": "New York",
      "name": "Bob Norman"
    },
    "items": [
      {
        "name": "Short Sleeve T-Shirt",
        "sku": "TS-001",
        "quantity": 1,
        "grams": 1000,
        "price": 1999,
        "vendor": "Acme",
        "product_id": 48447225880,
        "variant_id": 258644705304,
        "properties": null,
        "requires_shipping": true,
        "taxable": true
      }
    ],
    "currency": "USD",
    "locale": "en",
    "customer": {
      "id": 12345,
      "email": "bob@example.com",
      "first_name": "Bob",
      "last_name": "Norman",
      "tags": ["VIP"]
    }
  },
  "_datajet_source": "storefront_calculator",
  "_datajet_mode": "product"
}
```

Notes:

* **`destination`** — only includes keys whose form inputs are present and non-empty. Postal code is always present; the others are gated by block settings.
* **`items`** — for the **Product** block, contains a single item built from `product.selected_or_first_available_variant` at render time. For the **Cart** block, all line items are fetched from `/cart.js` at submit time.
* **`origin`** — **not included**. The storefront does not know the merchant's warehouse address. If your script needs `request.body.rate.origin`, hardcode it or fetch it from a DataJet [variable](/misc/variables.md).
* **`customer`** — `null` when the shopper is logged out. Otherwise contains `id`, `email`, `first_name`, `last_name`, `tags`.
* **`_datajet_source`** — always `"storefront_calculator"`. Use this to distinguish calls from the block from real Shopify Carrier Service calls.
* **`_datajet_mode`** — `"product"` or `"cart"`.

### Distinguishing block calls in your script

If you want different logic when the block calls vs. when Shopify's Carrier Service calls at checkout:

```liquid
{% if request.body._datajet_source == "storefront_calculator" %}
  {% comment %} request from the storefront block — origin is missing {% endcomment %}
{% else %}
  {% comment %} request from Shopify Carrier Service at checkout {% endcomment %}
{% endif %}
```

## Response Format

The block expects the same response your shipping\_rate script already returns:

```json
{
  "rates": [
    {
      "service_name": "Standard",
      "service_code": "STD",
      "total_price": "1295",
      "currency": "USD",
      "description": "5–7 business days"
    }
  ]
}
```

`total_price` is in cents (matching Shopify's convention). The block formats it via `Intl.NumberFormat` using the rate's `currency`. If `description` is present, it's rendered under the rate name.

States the block renders:

| Condition                       | UI                                                                             |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `rates` array has entries       | List of rates, one per row, with name + formatted price + optional description |
| `rates` is empty                | Empty-state copy ("No shipping options available for this address.")           |
| Non-2xx response or fetch error | Error copy ("Could not calculate shipping. Please try again.")                 |

## Example: Different Rates for Block vs. Checkout

A typical pattern is to return broad estimates from the block but precise per-carrier rates at checkout:

```liquid
{% if request.body._datajet_source == "storefront_calculator" %}
  {% json response %}
    {
      "body": {
        "rates": [
          { "service_name": "Standard (estimate)", "service_code": "EST", "total_price": "999", "currency": "USD" }
        ]
      }
    }
  {% endjson %}
{% else %}
  {% comment %} full carrier lookup for real checkout request {% endcomment %}
  {% comment %} ... your existing logic ... {% endcomment %}
{% endif %}

{% return response %}
```

## Logged-In Customer Data

When the shopper is logged in, the block renders customer info server-side via Liquid's `customer` global and includes it in `request.body.rate.customer`. This means:

* `customer.tags` is available **without** needing the [Shipping Rate Context](/scripts/blank/shipping-rate/shipping-rate-context.md) checkout extension. The block delivers tags directly in the payload.
* If `prefill_customer_address` is enabled and `customer.default_address` exists, the form inputs are pre-populated.

## Limitations

* **Variant changes on PDP** — the Product block snapshots the current variant at server-render time. If the shopper switches variants on the PDP, the payload still references the originally rendered variant until the page reloads.
* **One block per page** — multiple instances on the same page work, but each makes its own request. There is no shared state.
* **No origin** — the block does not provide `request.body.rate.origin`. Scripts that depend on it must source it elsewhere.
* **App proxy only** — the request goes through the Shopify app proxy. Make sure your app proxy is configured in `shopify.app.toml` and the subpath in the block's JS asset matches.

## Troubleshooting

#### Form submits but no rates appear

* Confirm a shipping\_rate script is **active** in DataJet.
* Check the **script ID** in block settings matches the active script.
* Inspect the script's run logs in DataJet — look for the request body and any errors.

#### Error message appears immediately

* Open the browser Network tab and inspect the `POST /apps/{proxy}/task/{id}` response.
* `404` usually means the app proxy subpath in the block's JS does not match `shopify.app.toml`'s `[app_proxy].subpath`.
* `5xx` means the script threw — check DataJet's script logs.

#### Customer fields don't pre-fill

* Pre-fill only works when the shopper is logged in **and** has a default address saved on their Shopify customer profile.
* Confirm `prefill_customer_address` is checked in block settings.

#### Cart block sends wrong line items

* The cart is fetched from `/cart.js` at submit time. If items look stale, force a hard reload to clear any cached cart state.
