Hide courier delivery options for PO box addresses
A Shopify delivery customization Function that hides UPS, FedEx and other courier options when the shipping address is a PO box. Tested JavaScript.
- Function API
- Delivery Customization
- Target
- cart.delivery-options.transform.run
- Plan
- Any plan in a public app; Shopify Plus in a custom app
- App scope
- write_delivery_customizations
- API version
- 2026-10
- Last checked
In short
This delivery customization Function hides courier options such as UPS, FedEx and DHL when the shipping address is a PO box, and keeps postal options available. Courier names come from a JSON metafield. It uses the cart.delivery-options.transform.run target of the Delivery Customization API, version 2026-10. Shopify notes the address may not be available for its built-in shipping rates.
How to use it
- Create an app with the Shopify CLI, then add the extension: shopify app generate extension --template delivery_customization, with JavaScript.
- Replace the generated shopify.extension.toml and the two files in src/ with the files below.
- Run the fixtures with shopify app function run --input tests/po-box-address.input.json.
- Deploy with shopify app deploy and create the customization with deliveryCustomizationCreate.
- Optionally list your couriers in the $app:function-configuration metafield, for example {"optionTitles": ["UPS", "FedEx"]}.
Code
shopify.extension.tomlapi_version = "2026-10"
[[extensions]]
name = "Hide delivery options for PO boxes"
handle = "hide-delivery-po-boxes"
type = "function"
[[extensions.targeting]]
target = "cart.delivery-options.transform.run"
input_query = "src/cart_delivery_options_transform_run.graphql"
export = "cart-delivery-options-transform-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
src/cart_delivery_options_transform_run.graphqlquery CartDeliveryOptionsTransformRunInput {
cart {
deliveryGroups {
deliveryAddress {
address1
address2
}
deliveryOptions {
handle
title
}
}
}
deliveryCustomization {
metafield(namespace: "$app", key: "function-configuration") {
jsonValue
}
}
}
src/cart_delivery_options_transform_run.js// @ts-check
/**
* @typedef {import("../generated/api").CartDeliveryOptionsTransformRunInput} CartDeliveryOptionsTransformRunInput
* @typedef {import("../generated/api").CartDeliveryOptionsTransformRunResult} CartDeliveryOptionsTransformRunResult
*/
/** Settings stored as JSON in the customization's metafield. */
const DEFAULT_CONFIG = {
// Couriers that don't deliver to PO boxes, matched case-insensitively anywhere in the option title.
optionTitles: ["UPS", "FedEx", "DHL", "Express", "Courier"],
};
/**
* "PO box" spellings anywhere in the line, after a space or punctuation:
* "P.O. Box", "PO Box", "Post Office Box", "Postfach", Turkish "Posta Kutusu"
* and "PK" followed by a number ("Kadıköy PK 34"). The leading boundary keeps
* words such as "Spk" or "Park" from matching.
*/
const PO_BOX =
/(?:^|[^\p{L}\p{N}])(?:p\.?\s*o\.?\s*box|post\s*office\s*box|post\s*box|postfach|posta\s*kutusu|p\.?\s*k\.?\s*\d)/iu;
/** @param {string | null | undefined} line */
const isPoBox = (line) => PO_BOX.test(line ?? "");
/**
* @param {CartDeliveryOptionsTransformRunInput} input
* @returns {CartDeliveryOptionsTransformRunResult}
*/
export function cartDeliveryOptionsTransformRun(input) {
const config = { ...DEFAULT_CONFIG, ...input.deliveryCustomization?.metafield?.jsonValue };
const titles = config.optionTitles.map((title) => title.toLowerCase());
const operations = input.cart.deliveryGroups
.filter((group) => isPoBox(group.deliveryAddress?.address1) || isPoBox(group.deliveryAddress?.address2))
.flatMap((group) => group.deliveryOptions)
.filter((option) => titles.some((title) => (option.title ?? "").toLowerCase().includes(title)))
.map((option) => ({ deliveryOptionHide: { deliveryOptionHandle: option.handle } }));
return { operations };
}
Test input and expected output
tests/configured-couriers.input.json{
"cart": {
"deliveryGroups": [
{
"deliveryAddress": { "address1": "Atatürk Cad. No: 5", "address2": "PK 112" },
"deliveryOptions": [
{ "handle": "77ff-ptt", "title": "PTT Kargo" },
{ "handle": "77ff-yurtici", "title": "Yurtiçi Kargo" }
]
}
]
},
"deliveryCustomization": { "metafield": { "jsonValue": { "optionTitles": ["Yurtiçi"] } } }
}
tests/configured-couriers.output.json{ "operations": [ { "deliveryOptionHide": { "deliveryOptionHandle": "77ff-yurtici" } } ] }
tests/po-box-address.input.json{
"cart": {
"deliveryGroups": [
{ "deliveryAddress": { "address1": "P.O. Box 4521", "address2": null }, "deliveryOptions": [
{ "handle": "c0de-usps", "title": "USPS Priority Mail" },
{ "handle": "c0de-ups", "title": "UPS Ground" },
{ "handle": "c0de-fedex", "title": "FedEx 2Day" }
] }
]
},
"deliveryCustomization": { "metafield": null }
}
tests/po-box-address.output.json{
"operations": [
{ "deliveryOptionHide": { "deliveryOptionHandle": "c0de-ups" } },
{ "deliveryOptionHide": { "deliveryOptionHandle": "c0de-fedex" } }
]
}
tests/po-box-after-name.input.json{
"cart": {
"deliveryGroups": [
{
"deliveryAddress": {
"address1": "John Smith PO Box 5",
"address2": null
},
"deliveryOptions": [
{
"handle": "c0de-usps",
"title": "USPS Priority Mail"
},
{
"handle": "c0de-ups",
"title": "UPS Ground"
},
{
"handle": "c0de-fedex",
"title": "FedEx 2Day"
}
]
}
]
},
"deliveryCustomization": {
"metafield": null
}
}tests/po-box-after-name.output.json{
"operations": [
{ "deliveryOptionHide": { "deliveryOptionHandle": "c0de-ups" } },
{ "deliveryOptionHide": { "deliveryOptionHandle": "c0de-fedex" } }
]
}
tests/street-address.input.json{
"cart": {
"deliveryGroups": [
{ "deliveryAddress": { "address1": "120 Boxwood Lane", "address2": "Apt 4" }, "deliveryOptions": [
{ "handle": "c0de-usps", "title": "USPS Priority Mail" },
{ "handle": "c0de-ups", "title": "UPS Ground" },
{ "handle": "c0de-fedex", "title": "FedEx 2Day" }
] }
]
},
"deliveryCustomization": { "metafield": null }
}
tests/street-address.output.json{ "operations": [] }
Written by Lintel. MIT license: free to use in your own store and in client work.
Limits and edge cases
- Shopify’s reference warns that hiding options for PO boxes is only supported for rates from carrier services or shipping apps; with built-in rates the address data might not be available.
- PO box detection is a text check (“PO Box”, “P.O. Box”, “Post Office Box”, “Postfach”, Turkish “Posta Kutusu”/“PK”). Unusual spellings won’t match.
- To stop PO box orders altogether, use the validation example instead; it blocks checkout with a message.
Questions
- Why hide options instead of blocking PO boxes?
- Many stores can ship to PO boxes with the postal service but not with couriers. Hiding only the courier options keeps those orders possible.
- Does this work with Shopify’s own shipping rates?
- Not reliably. Shopify says the address needed to detect a PO box might not be available to the Function for built-in rates. It’s supported for carrier-calculated and app rates.
- Can I detect PO boxes in other languages?
- Yes. Extend the PO_BOX pattern in the code with the local spelling, for example “Apartado” for Spanish addresses, and add a fixture for it.
Shopify documentation
- Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
- Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
- Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
- MIT license. Use the code in your own store or in client work.
Need this built, tested and maintained for your store? See Shopify Functions development.
