Pipe17 Estimated Delivery Date Service Reference

 

Reference implementation. This guide is provided as an example only to illustrate how to integrate the EDD Service with a Shopify storefront. The code samples, theme snippets, and app proxy configuration shown here are starting points — you will need to adapt them to your specific store theme, branding, and production requirements. Pipe17 does not provide a pre-built Shopify app or managed proxy; you are responsible for building, hosting, and maintaining the integration described below.


Display estimated delivery dates on your Shopify product and cart pages to increase buyer confidence and drive conversion. This guide walks you through building a Shopify app that proxies requests to the Pipe17 Estimated Delivery Date (EDD) Service and rendering delivery estimates in your store theme.

 


How it works

The integration consists of three components:

Shopify Storefront (Theme)
  |
  |  JavaScript fetch to /apps/brand-edd/delivery-options
  v
Shopify App Proxy
  |
  |  Proxies request to your app server
  v
Your Shopify App (React Router / Node.js)
  |
  |  POST /api/v1/{orgKey}/delivery-options
  |  Header: X-Pipe17-EDD-Key: {apiKey}
  v
EDD Service
  |
  |  Returns ranked delivery options
  v
Response flows back to the storefront and renders

Why use an App Proxy? App proxies solve two problems: they keep your EDD API credentials server-side (never exposed in browser JavaScript), and they avoid CORS issues since requests go through the store's own domain.

Prerequisites

Before you begin, make sure you have:

  • EDD Service deployed and accessible, with your organization configured (locations, carrier service levels, transit times, inventory)
  • Your Pipe17 org key (used as the URL parameter in requests to the EDD service)
  • Your Pipe17 EDD integration API key (used as the API auth key in requests to the EDD service)
  • Your Pipe17 selling channel integration ID (used as a body field in requests to the EDD service)
  • The EDD Service base URL (e.g., https://edd.example.com)
  • Shopify Partner account with access to create apps
  • Shopify development store for testing
  • Shopify CLI installed (npm install -g @shopify/cli)
  • Node.js 20+
  • Product variant SKUs in Shopify that match the SKUs configured in the EDD service

Step 1: Scaffold the Shopify app

Create a new Shopify app using the React Router template:

shopify app init
cd your-app-name

Follow the prompts to name your app and connect it to your Partner account.

Start the dev server:

shopify app dev

This starts the app and creates a tunnel so Shopify can reach your local server.


Step 2: Create the App Proxy route

The App Proxy route receives requests from the storefront, forwards them to the EDD Service, and returns the response.

2a. Add environment variables

Add these to your app's .env file:

# EDD Service configuration
EDD_BASE_URL=https://edd.example.com
EDD_ORG_KEY=your-Pipe17-org-key
EDD_API_KEY=your-Pipe17-EDD-integration-API-key
EDD_SELLING_CHANNEL_INTEGRATION_ID=your-Pipe17-selling-channel-integration-ID

2b. Create the proxy route

Create the file app/routes/apps.brand-edd.delivery-options.tsx:

import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "react-router";
import { authenticate } from "../shopify.server";

// Only POST is used for delivery options
export async function action({ request }: ActionFunctionArgs) {
  // Validate the request came through Shopify's App Proxy
  await authenticate.public.appProxy(request);

  if (request.method !== "POST") {
    return json({ success: false, error: { code: "METHOD_NOT_ALLOWED", message: "Use POST" } }, 405);
  }

  const eddBaseUrl = process.env.EDD_BASE_URL;
  const orgKey = process.env.EDD_ORG_KEY;
  const apiKey = process.env.EDD_API_KEY;
  const sellingChannelIntegrationId = process.env.EDD_SELLING_CHANNEL_INTEGRATION_ID;

  if (!eddBaseUrl || !orgKey || !apiKey || !sellingChannelIntegrationId) {
    console.error("Missing EDD_BASE_URL or EDD_ORG_KEY or EDD_API_KEY or EDD_SELLING_CHANNEL_INTEGRATION_ID env vars");
    return json(
      { success: false, error: { code: "CONFIG_ERROR", message: "EDD service not configured" } },
      500
    );
  }

  try {
    const body = await request.json();
    // Required by EDD service
    body.sellingChannelIntegrationId = sellingChannelIntegrationId;

    // Forward the request to the EDD Service
    const eddResponse = await fetch(`${eddBaseUrl}/api/v1/${orgKey}/delivery-options`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Pipe17-EDD-Key": apiKey,
      },
      body: JSON.stringify(body),
    });

    const data = await eddResponse.json();
    return json(data, eddResponse.status);
  } catch (error) {
    console.error("EDD proxy error:", error);
    return json(
      { success: false, error: { code: "PROXY_ERROR", message: "Failed to reach EDD service" } },
      502
    );
  }
}

// GET requests return a simple health check
export async function loader({ request }: LoaderFunctionArgs) {
  await authenticate.public.appProxy(request);
  return json({ status: "ok", service: "brand-edd-proxy" });
}

Note: The route filename apps.brand-edd.delivery-options.tsx maps to the URL path /apps/brand-edd/delivery-options in React Router's file-based routing. The dots in the filename become / separators in the URL.


Step 3: Configure the App Proxy

3a. Update shopify.app.toml

Add the app proxy configuration and required access scope:

[access_scopes]
scopes = "write_app_proxy"

[app_proxy]
url = "/apps/brand-edd"
prefix = "apps"
subpath = "brand-edd"

This means requests to https://your-store.myshopify.com/apps/brand-edd/* are proxied to your app at https://your-app-url/apps/brand-edd/*.

3b. Deploy and install

During development, shopify app dev handles the proxy automatically. For production:

shopify app deploy

Then install the app on your store from the Partner Dashboard.


Step 4: Add the product page widget

This adds a "Check estimated delivery" widget on the product detail page. The customer enters their ZIP code and sees delivery options for the selected variant.

 

Screenshot 2026-03-23 at 2.21.22 PM.png

 

4a. Create the snippet

Create the file snippets/brand-edd-widget.liquid in your Shopify theme:

{% doc %}
  Renders an estimated delivery date widget that calls the EDD service
  via the app proxy. Displays delivery options for the current product variant.

  @param {product} product - The product to show delivery estimates for

  @example
  {% render 'brand-edd-widget', product: product %}
{% enddoc %}

<div id="brand-edd-widget" class="brand-edd" style="display:none;">
  <div class="brand-edd__input-row">
    <label class="brand-edd__label" for="brand-edd-zip">
      Check estimated delivery
    </label>
    <div class="brand-edd__input-group">
      <input
        type="text"
        id="brand-edd-zip"
        class="brand-edd__zip-input"
        placeholder="Enter ZIP code"
        maxlength="10"
        inputmode="numeric"
        autocomplete="postal-code"
      >
      <button
        type="button"
        id="brand-edd-check"
        class="brand-edd__button"
      >
        Check
      </button>
    </div>
  </div>

  <div id="brand-edd-results" class="brand-edd__results" aria-live="polite"></div>
</div>

<script id="brand-edd-variant-data" type="application/json">
  [
    {%- for variant in product.variants -%}
      {
        "id": {{ variant.id | json }},
        "sku": {{ variant.sku | json }},
        "available": {{ variant.available | json }}
      }{% unless forloop.last %},{% endunless %}
    {%- endfor -%}
  ]
</script>

{% stylesheet %}
  .brand-edd {
    margin: 16px 0;
    padding: 16px;
    border: 1px solid #e5e5e5;
    border-radius: 8px;
    font-family: inherit;
  }

  .brand-edd__label {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    margin-bottom: 8px;
    color: #333;
  }

  .brand-edd__input-group {
    display: flex;
    gap: 8px;
  }

  .brand-edd__zip-input {
    flex: 1;
    max-width: 160px;
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 0.9rem;
  }

  .brand-edd__zip-input:focus {
    outline: none;
    border-color: #333;
  }

  .brand-edd__button {
    padding: 8px 20px;
    background: #333;
    color: #fff;
    border: none;
    border-radius: 4px;
    font-size: 0.9rem;
    cursor: pointer;
  }

  .brand-edd__button:hover {
    background: #555;
  }

  .brand-edd__button:disabled {
    background: #999;
    cursor: not-allowed;
  }

  .brand-edd__results {
    margin-top: 12px;
  }

  .brand-edd__option {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
    font-size: 0.9rem;
  }

  .brand-edd__option:last-child {
    border-bottom: none;
  }

  .brand-edd__date {
    font-weight: 600;
    color: #111;
  }

  .brand-edd__carrier {
    color: #666;
    font-size: 0.8rem;
  }

  .brand-edd__days {
    color: #2e7d32;
    font-weight: 500;
    white-space: nowrap;
  }

  .brand-edd__split-badge {
    display: inline-block;
    font-size: 0.7rem;
    background: #fff3e0;
    color: #e65100;
    padding: 2px 6px;
    border-radius: 3px;
    margin-left: 6px;
  }

  .brand-edd__error {
    color: #c62828;
    font-size: 0.85rem;
    padding: 8px 0;
  }

  .brand-edd__loading {
    color: #666;
    font-size: 0.85rem;
    padding: 8px 0;
  }

  .brand-edd__empty {
    color: #666;
    font-size: 0.85rem;
    padding: 8px 0;
  }
{% endstylesheet %}

{% javascript %}
  (function() {
    var PROXY_URL = "/apps/brand-edd/delivery-options";

    var widget = document.getElementById("brand-edd-widget");
    var zipInput = document.getElementById("brand-edd-zip");
    var checkBtn = document.getElementById("brand-edd-check");
    var resultsDiv = document.getElementById("brand-edd-results");

    // Parse variant data embedded by Liquid
    var variantData = [];
    try {
      var dataEl = document.getElementById("brand-edd-variant-data");
      if (dataEl) variantData = JSON.parse(dataEl.textContent);
    } catch (e) {
      console.error("EDD: Failed to parse variant data", e);
    }

    // Show the widget once JS is ready
    if (widget) widget.style.display = "block";

    // Get the currently selected variant ID from the URL or form
    function getSelectedVariantId() {
      // Check URL params first (Shopify default behavior)
      var params = new URLSearchParams(window.location.search);
      var variantId = params.get("variant");
      if (variantId) return parseInt(variantId, 10);

      // Fallback: check for a hidden input in the product form
      var input = document.querySelector('form[action="/cart/add"] input[name="id"]');
      if (input) return parseInt(input.value, 10);

      // Fallback: first available variant
      for (var i = 0; i < variantData.length; i++) {
        if (variantData[i].available) return variantData[i].id;
      }
      return null;
    }

    // Find variant SKU by ID
    function getVariantSku(variantId) {
      for (var i = 0; i < variantData.length; i++) {
        if (variantData[i].id === variantId) return variantData[i].sku;
      }
      return null;
    }

    // Format a date string for display
    function formatDate(isoString) {
      var date = new Date(isoString);
      return date.toLocaleDateString("en-US", {
        weekday: "short",
        month: "short",
        day: "numeric"
      });
    }

    // Format carrier service level code for display
    function formatCarrier(code) {
      return code
        .replace(/_/g, " ")
        .replace(/\b\w/g, function(c) { return c.toUpperCase(); });
    }

    // Render delivery options
    function renderResults(data) {
      if (!data.success || !data.data || !data.data.options) {
        resultsDiv.innerHTML = '<div class="brand-edd__error">Unable to calculate delivery estimate.</div>';
        return;
      }

      var options = data.data.options;
      if (options.length === 0) {
        resultsDiv.innerHTML = '<div class="brand-edd__empty">No delivery options available for this ZIP code.</div>';
        return;
      }

      var html = "";
      for (var i = 0; i < options.length; i++) {
        var opt = options[i];
        var daysLabel = opt.estimatedDays === 0
          ? "Today"
          : opt.estimatedDays === 1
            ? "Tomorrow"
            : opt.estimatedDays + " business days";
        var splitBadge = opt.isSplit
          ? '<span class="brand-edd__split-badge">Ships from multiple locations</span>'
          : "";

        html += '<div class="brand-edd__option">'
          + '  <div>'
          + '    <span class="brand-edd__date">Get it by ' + formatDate(opt.promiseDate) + '</span>'
          + '    <span class="brand-edd__carrier"> &mdash; ' + formatCarrier(opt.carrierServiceLevelCode) + '</span>'
          + splitBadge
          + '  </div>'
          + '  <div class="brand-edd__days">' + daysLabel + '</div>'
          + '</div>';
      }
      resultsDiv.innerHTML = html;
    }

    // Fetch delivery options from the App Proxy
    function fetchDeliveryOptions(zip) {
      var variantId = getSelectedVariantId();
      var sku = variantId ? getVariantSku(variantId) : null;

      if (!sku) {
        resultsDiv.innerHTML = '<div class="brand-edd__error">No SKU found for this variant.</div>';
        return;
      }

      resultsDiv.innerHTML = '<div class="brand-edd__loading">Checking delivery options&hellip;</div>';
      checkBtn.disabled = true;

      fetch(PROXY_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          destination: {
            postalCode: zip,
            country: "US"
          },
          items: [
            { sku: sku, quantity: 1 }
          ],
          constraints: {
            maxOptions: 3,
            maxSplits: 2
          }
        })
      })
      .then(function(res) { return res.json(); })
      .then(function(data) { renderResults(data); })
      .catch(function(err) {
        console.error("EDD fetch error:", err);
        resultsDiv.innerHTML = '<div class="brand-edd__error">Unable to check delivery. Please try again.</div>';
      })
      .finally(function() { checkBtn.disabled = false; });
    }

    // Event listeners
    if (checkBtn) {
      checkBtn.addEventListener("click", function() {
        var zip = zipInput.value.trim();
        if (zip.length < 3) {
          resultsDiv.innerHTML = '<div class="brand-edd__error">Please enter a valid ZIP code.</div>';
          return;
        }
        fetchDeliveryOptions(zip);
      });
    }

    if (zipInput) {
      zipInput.addEventListener("keydown", function(e) {
        if (e.key === "Enter") {
          e.preventDefault();
          checkBtn.click();
        }
      });
    }

    // Re-fetch when variant changes (listen for Shopify's standard variant change event)
    window.addEventListener("popstate", function() {
      var zip = zipInput.value.trim();
      if (zip.length >= 3) fetchDeliveryOptions(zip);
    });
  })();
{% endjavascript %}

4b. Add the snippet to your product template

Edit your product template (typically sections/main-product.liquid or similar) and add the snippet where you want the widget to appear. A good location is below the "Add to cart" button:

{% render 'brand-edd-widget', product: product %}

Step 5: Add the cart page widget

The cart page widget shows delivery estimates for all items in the cart. It reuses the same App Proxy endpoint but sends all cart line items.

 

Screenshot 2026-03-23 at 2.21.35 PM.png

 

5a. Create the cart snippet

Create the file snippets/brand-edd-cart.liquid in your theme:

{% doc %}
  Renders estimated delivery dates for all items in the cart.
  Prompts the customer for a ZIP code, then displays delivery options
  for the entire cart contents.

  @example
  {% render 'brand-edd-cart' %}
{% enddoc %}

{% if cart.item_count > 0 %}
  <div id="brand-edd-cart-widget" class="brand-edd-cart" style="display:none;">
    <h3 class="brand-edd-cart__title">Estimated delivery</h3>

    <div class="brand-edd-cart__input-row">
      <input
        type="text"
        id="brand-edd-cart-zip"
        class="brand-edd-cart__zip-input"
        placeholder="Enter ZIP code"
        maxlength="10"
        inputmode="numeric"
        autocomplete="postal-code"
      >
      <button
        type="button"
        id="brand-edd-cart-check"
        class="brand-edd-cart__button"
      >
        Check
      </button>
    </div>

    <div id="brand-edd-cart-results" class="brand-edd-cart__results" aria-live="polite"></div>
  </div>

  <script id="brand-edd-cart-items" type="application/json">
    [
      {%- for item in cart.items -%}
        {
          "sku": {{ item.sku | json }},
          "quantity": {{ item.quantity | json }},
          "title": {{ item.title | json }}
        }{% unless forloop.last %},{% endunless %}
      {%- endfor -%}
    ]
  </script>
{% endif %}

{% stylesheet %}
  .brand-edd-cart {
    margin: 20px 0;
    padding: 16px;
    border: 1px solid #e5e5e5;
    border-radius: 8px;
  }

  .brand-edd-cart__title {
    font-size: 1rem;
    font-weight: 600;
    margin: 0 0 12px 0;
  }

  .brand-edd-cart__input-row {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
  }

  .brand-edd-cart__zip-input {
    flex: 1;
    max-width: 160px;
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 0.9rem;
  }

  .brand-edd-cart__zip-input:focus {
    outline: none;
    border-color: #333;
  }

  .brand-edd-cart__button {
    padding: 8px 20px;
    background: #333;
    color: #fff;
    border: none;
    border-radius: 4px;
    font-size: 0.9rem;
    cursor: pointer;
  }

  .brand-edd-cart__button:hover {
    background: #555;
  }

  .brand-edd-cart__button:disabled {
    background: #999;
    cursor: not-allowed;
  }

  .brand-edd-cart__results {
    font-size: 0.9rem;
  }

  .brand-edd-cart__option {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid #f0f0f0;
  }

  .brand-edd-cart__option:last-child {
    border-bottom: none;
  }

  .brand-edd-cart__date {
    font-weight: 600;
  }

  .brand-edd-cart__carrier {
    color: #666;
    font-size: 0.8rem;
  }

  .brand-edd-cart__days {
    color: #2e7d32;
    font-weight: 500;
  }

  .brand-edd-cart__split-badge {
    display: inline-block;
    font-size: 0.7rem;
    background: #fff3e0;
    color: #e65100;
    padding: 2px 6px;
    border-radius: 3px;
    margin-left: 6px;
  }

  .brand-edd-cart__error {
    color: #c62828;
    font-size: 0.85rem;
  }

  .brand-edd-cart__loading {
    color: #666;
    font-size: 0.85rem;
  }

  .brand-edd-cart__empty {
    color: #666;
    font-size: 0.85rem;
  }
{% endstylesheet %}

{% javascript %}
  (function() {
    var PROXY_URL = "/apps/brand-edd/delivery-options";

    var widget = document.getElementById("brand-edd-cart-widget");
    var zipInput = document.getElementById("brand-edd-cart-zip");
    var checkBtn = document.getElementById("brand-edd-cart-check");
    var resultsDiv = document.getElementById("brand-edd-cart-results");

    // Parse cart item data embedded by Liquid
    var cartItems = [];
    try {
      var dataEl = document.getElementById("brand-edd-cart-items");
      if (dataEl) cartItems = JSON.parse(dataEl.textContent);
    } catch (e) {
      console.error("EDD Cart: Failed to parse cart data", e);
    }

    // Filter out items without SKUs
    var validItems = cartItems.filter(function(item) {
      return item.sku && item.sku.length > 0;
    });

    // Only show the widget if we have items with SKUs
    if (widget && validItems.length > 0) widget.style.display = "block";

    function formatDate(isoString) {
      var date = new Date(isoString);
      return date.toLocaleDateString("en-US", {
        weekday: "short",
        month: "short",
        day: "numeric"
      });
    }

    function formatCarrier(code) {
      return code.replace(/_/g, " ").replace(/\b\w/g, function(c) { return c.toUpperCase(); });
    }

    function renderResults(data) {
      if (!data.success || !data.data || !data.data.options) {
        resultsDiv.innerHTML = '<div class="brand-edd-cart__error">Unable to calculate delivery estimate.</div>';
        return;
      }

      var options = data.data.options;
      if (options.length === 0) {
        resultsDiv.innerHTML = '<div class="brand-edd-cart__empty">No delivery options available for this ZIP code.</div>';
        return;
      }

      var html = "";
      for (var i = 0; i < options.length; i++) {
        var opt = options[i];
        var daysLabel = opt.estimatedDays === 0 ? "Today"
          : opt.estimatedDays === 1 ? "Tomorrow"
          : opt.estimatedDays + " business days";
        var splitBadge = opt.isSplit
          ? '<span class="brand-edd-cart__split-badge">Multiple shipments</span>'
          : "";

        html += '<div class="brand-edd-cart__option">'
          + '<div>'
          + '  <span class="brand-edd-cart__date">Get it by ' + formatDate(opt.promiseDate) + '</span>'
          + '  <span class="brand-edd-cart__carrier"> &mdash; ' + formatCarrier(opt.carrierServiceLevelCode) + '</span>'
          + splitBadge
          + '</div>'
          + '<div class="brand-edd-cart__days">' + daysLabel + '</div>'
          + '</div>';
      }
      resultsDiv.innerHTML = html;
    }

    function fetchDeliveryOptions(zip) {
      var items = validItems.map(function(item) {
        return { sku: item.sku, quantity: item.quantity };
      });

      resultsDiv.innerHTML = '<div class="brand-edd-cart__loading">Checking delivery options&hellip;</div>';
      checkBtn.disabled = true;

      fetch(PROXY_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          destination: {
            postalCode: zip,
            country: "US"
          },
          items: items,
          constraints: {
            maxOptions: 3,
            maxSplits: 2
          }
        })
      })
      .then(function(res) { return res.json(); })
      .then(function(data) { renderResults(data); })
      .catch(function(err) {
        console.error("EDD Cart fetch error:", err);
        resultsDiv.innerHTML = '<div class="brand-edd-cart__error">Unable to check delivery. Please try again.</div>';
      })
      .finally(function() { checkBtn.disabled = false; });
    }

    if (checkBtn) {
      checkBtn.addEventListener("click", function() {
        var zip = zipInput.value.trim();
        if (zip.length < 3) {
          resultsDiv.innerHTML = '<div class="brand-edd-cart__error">Please enter a valid ZIP code.</div>';
          return;
        }
        fetchDeliveryOptions(zip);
      });
    }

    if (zipInput) {
      zipInput.addEventListener("keydown", function(e) {
        if (e.key === "Enter") {
          e.preventDefault();
          checkBtn.click();
        }
      });
    }
  })();
{% endjavascript %}

5b. Add the snippet to your cart template

Edit your cart template (typically sections/main-cart.liquid or similar) and add:

{% render 'brand-edd-cart' %}

Place it above the checkout button for maximum visibility.


Step 6: Testing

Test the App Proxy directly

With shopify app dev running, open your browser and verify the proxy is working:

# Health check (GET)
curl https://your-store.myshopify.com/apps/brand-edd/delivery-options
# Expected: {"status":"ok","service":"brand-edd-proxy"}

# Delivery options (POST)
curl -X POST https://your-store.myshopify.com/apps/brand-edd/delivery-options \
  -H "Content-Type: application/json" \
  -d '{
    "destination": { "postalCode": "10001", "country": "US" },
    "items": [{ "sku": "TEST-SKU-001", "quantity": 1 }]
  }'

Test on the storefront

  1. Navigate to a product page on your dev store
  2. The "Check estimated delivery" widget should appear below the product form
  3. Enter a US ZIP code (e.g., 10001) and click Check
  4. Verify that delivery options display with dates, carriers, and day counts
  5. Navigate to the cart page with items added
  6. Verify the cart delivery widget appears and works the same way

Verify SKU mapping

Make sure the variant SKUs in Shopify match the SKUs configured in the EDD service. You can check this in the Shopify admin under Products > [Product] > Variants and compare against the inventory levels configured in the EDD service.

If a variant has no SKU set, the widget will display "No SKU found for this variant."


API reference summary

Request

POST /api/v1/{org-key-in-Pipe17}/delivery-options
Header: X-Pipe17-EDD-Key: {EDD-integration-API-key-in-Pipe17}
Content-Type: application/json
{
  "sellingChannelIntegrationId": "{selling-channel-integration-ID-in-Pipe17}",
  "destination": {
    "postalCode": "10001",
    "country": "US",
    "state": "NY",
    "city": "New York"
  },
  "items": [
    { "sku": "PRODUCT-SKU-001", "quantity": 2 },
    { "sku": "PRODUCT-SKU-002", "quantity": 1 }
  ],
  "constraints": {
    "maxOptions": 3,
    "maxSplits": 2,
    "serviceLevelCodes": ["FEDEX_GROUND", "UPS_2DAY"]
  }
}
FieldRequiredDescription
sellingChannelIntegrationIdYesSelling channel integration ID in Pipe17
  destination.postalCode  Yes  Customer's ZIP/postal code (min 3 chars)
destination.countryYesCountry code (e.g., US)
destination.stateNoState/province code
destination.cityNoCity name
items[].skuYesProduct variant SKU
items[].quantityYesQuantity (>= 1)
constraints.maxOptionsNoMax delivery options to return (default: 3, max: 10)
constraints.maxSplitsNoMax split-shipment locations (default: 2)
constraints.serviceLevelCodesNoFilter to specific carrier service levels
constraints.diagnosticsNoInclude debug info (default: false)

Response

{
  "success": true,
  "data": {
    "orgKey": "your-org-key",
    "options": [
      {
        "rank": 1,
        "locations": [
          {
            "locationId": "warehouse-east",
            "facilityName": "East Coast DC",
            "items": [
              { "sku": "PRODUCT-SKU-001", "quantity": 2 },
              { "sku": "PRODUCT-SKU-002", "quantity": 1 }
            ]
          }
        ],
        "promiseDate": "2026-03-26T00:00:00.000Z",
        "estimatedDays": 3,
        "isSplit": false,
        "carrierServiceLevelCode": "FEDEX_GROUND",
        "shipments": [
          {
            "locationId": "warehouse-east",
            "shipDate": "2026-03-23T16:00:00.000Z",
            "estimatedDeliveryDate": "2026-03-26T00:00:00.000Z",
            "transitDays": 3
          }
        ]
      },
      {
        "rank": 2,
        "locations": [
          {
            "locationId": "warehouse-east",
            "items": [{ "sku": "PRODUCT-SKU-001", "quantity": 2 }]
          },
          {
            "locationId": "warehouse-west",
            "items": [{ "sku": "PRODUCT-SKU-002", "quantity": 1 }]
          }
        ],
        "promiseDate": "2026-03-25T00:00:00.000Z",
        "estimatedDays": 2,
        "isSplit": true,
        "carrierServiceLevelCode": "FEDEX_2DAY",
        "shipments": [
          {
            "locationId": "warehouse-east",
            "shipDate": "2026-03-23T16:00:00.000Z",
            "estimatedDeliveryDate": "2026-03-25T00:00:00.000Z",
            "transitDays": 2
          },
          {
            "locationId": "warehouse-west",
            "shipDate": "2026-03-23T18:00:00.000Z",
            "estimatedDeliveryDate": "2026-03-25T00:00:00.000Z",
            "transitDays": 2
          }
        ]
      }
    ]
  }
}
FieldDescription
options[].rank1 = fastest option
options[].promiseDateLatest delivery date (ISO 8601)
options[].estimatedDaysBusiness days until delivery
options[].isSplittrue if shipping from multiple locations
options[].carrierServiceLevelCodeCarrier and service (e.g., FEDEX_GROUND)
options[].locations[]Which items ship from where
options[].shipments[]Per-location ship dates and transit times

An empty options array means no valid delivery options exist for the given destination and items. This is a successful response, not an error.


Troubleshooting

Widget doesn't appear

  • Verify the snippet {% render 'brand-edd-widget', product: product %} is inside a section that has access to the product object
  • Check the browser console for JavaScript errors
  • Ensure the theme file was saved and the page was hard-refreshed

"No SKU found for this variant"

  • Open the product in Shopify admin and confirm the variant has a SKU set
  • Verify the SKU matches what's configured in the EDD service

"Unable to check delivery"

  • Open the browser Network tab and look for the POST to /apps/brand-edd/delivery-options
  • If you see a 404: the App Proxy isn't configured correctly. Verify shopify.app.toml has the [app_proxy] section and the app is installed
  • If you see a 502: the proxy can't reach the EDD service. Check the EDD_BASE_URL env var and that the EDD service is running
  • If you see a 401: the EDD_ORG_KEY doesn't match. Verify it matches the org key in the EDD service

"No delivery options available"

This is a valid response meaning the EDD service found no fulfillable options. Check:

  • The org has active locations with inventory for the requested SKUs
  • Carrier service levels are configured with transit times for the destination ZIP
  • The destination ZIP is in a serviced region

App Proxy returns HTML instead of JSON

Ensure your proxy route returns json() responses, not Liquid. Shopify only renders Liquid when the response has Content-Type: application/liquid.


Next steps

  • Customize styling: Update the CSS in the snippets to match your store theme
  • Add geolocation: Use the browser Geolocation API to pre-fill the ZIP code
  • Cache results: Add a short TTL cache in the proxy route to reduce EDD API calls for repeated ZIP codes
  • International support: Extend the country field to support non-US destinations if your EDD service is configured for them
  • Analytics: Track widget impressions and delivery option selections to measure conversion impact
Was this article helpful?
0 out of 0 found this helpful

Comments

0 comments

Please sign in to leave a comment.

Have more questions?
Submit a request
Share it, if you like it.