# 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.datajet-app.com/scripts/blank/shipping-rate/shipping-calculator-block.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
