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.tsxmaps to the URL path/apps/brand-edd/delivery-optionsin 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.
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"> — ' + 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…</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.
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"> — ' + 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…</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
- Navigate to a product page on your dev store
- The "Check estimated delivery" widget should appear below the product form
- Enter a US ZIP code (e.g.,
10001) and click Check - Verify that delivery options display with dates, carriers, and day counts
- Navigate to the cart page with items added
- 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"]
}
}
| Field | Required | Description |
|---|---|---|
sellingChannelIntegrationId | Yes | Selling channel integration ID in Pipe17 |
destination.postalCode | Yes | Customer's ZIP/postal code (min 3 chars) |
destination.country | Yes | Country code (e.g., US) |
destination.state | No | State/province code |
destination.city | No | City name |
items[].sku | Yes | Product variant SKU |
items[].quantity | Yes | Quantity (>= 1) |
constraints.maxOptions | No | Max delivery options to return (default: 3, max: 10) |
constraints.maxSplits | No | Max split-shipment locations (default: 2) |
constraints.serviceLevelCodes | No | Filter to specific carrier service levels |
constraints.diagnostics | No | Include 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
}
]
}
]
}
}
| Field | Description |
|---|---|
options[].rank | 1 = fastest option |
options[].promiseDate | Latest delivery date (ISO 8601) |
options[].estimatedDays | Business days until delivery |
options[].isSplit | true if shipping from multiple locations |
options[].carrierServiceLevelCode | Carrier 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 theproductobject - 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.tomlhas 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_URLenv var and that the EDD service is running - If you see a 401: the
EDD_ORG_KEYdoesn'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
countryfield 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
Comments
0 comments