Block PO box shipping addresses
A Shopify cart and checkout validation Function that stops checkout when the shipping address is a PO box and shows a message on the address field.
- Function API
- Cart and Checkout Validation
- Target
- cart.validations.generate.run
- Plan
- Any plan in a public app; Shopify Plus in a custom app
- App scope
- write_validations
- API version
- 2026-10
- Last checked
In short
This validation Function stops checkout when the shipping address is a PO box and shows your message on the address line field. It recognises common spellings such as “PO Box”, “P.O. Box” and “Post Office Box”. It uses the cart.validations.generate.run target of the Cart and Checkout Validation API, version 2026-10, and runs on any plan in a public app.
How to use it
- Create an app with the Shopify CLI, then add the extension: shopify app generate extension --template cart_checkout_validation, 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-in-address1.input.json.
- Deploy with shopify app deploy and turn the validation on with validationCreate.
- Optionally set your own message in the validation’s $app:function-configuration metafield: {"message": "…"}.
Code
shopify.extension.tomlapi_version = "2026-10"
[[extensions]]
name = "Block PO box addresses"
handle = "block-po-box-addresses"
type = "function"
[[extensions.targeting]]
target = "cart.validations.generate.run"
input_query = "src/cart_validations_generate_run.graphql"
export = "cart-validations-generate-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
src/cart_validations_generate_run.graphqlquery CartValidationsGenerateRunInput {
cart {
deliveryGroups {
deliveryAddress {
address1
address2
}
}
}
validation {
metafield(namespace: "$app", key: "function-configuration") {
jsonValue
}
}
}
src/cart_validations_generate_run.js// @ts-check
/**
* @typedef {import("../generated/api").CartValidationsGenerateRunInput} CartValidationsGenerateRunInput
* @typedef {import("../generated/api").CartValidationsGenerateRunResult} CartValidationsGenerateRunResult
*/
/** Settings stored as JSON in the validation's metafield. */
const DEFAULT_CONFIG = {
message: "We can't ship to PO boxes. Please enter a street address.",
};
/**
* Checkout can point an error at the first delivery group's address only, so
* a PO box in any group is reported on that field.
*/
const ADDRESS1_TARGET = "$.cart.deliveryGroups[0].deliveryAddress.address1";
/**
* "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 ?? "");
/** @type {CartValidationsGenerateRunResult} */
const NO_ERRORS = { operations: [] };
/**
* @param {CartValidationsGenerateRunInput} input
* @returns {CartValidationsGenerateRunResult}
*/
export function cartValidationsGenerateRun(input) {
const config = { ...DEFAULT_CONFIG, ...input.validation?.metafield?.jsonValue };
const hasPoBox = input.cart.deliveryGroups.some(
(group) => isPoBox(group.deliveryAddress?.address1) || isPoBox(group.deliveryAddress?.address2),
);
if (!hasPoBox) return NO_ERRORS;
return {
operations: [{ validationAdd: { errors: [{ message: config.message, target: ADDRESS1_TARGET }] } }],
};
}
Test input and expected output
tests/po-box-after-district.input.json{
"cart": {
"deliveryGroups": [
{
"deliveryAddress": {
"address1": "Kadıköy PK 34",
"address2": null
}
}
]
},
"validation": {
"metafield": null
}
}tests/po-box-after-district.output.json{
"operations": [
{
"validationAdd": {
"errors": [
{
"message": "We can't ship to PO boxes. Please enter a street address.",
"target": "$.cart.deliveryGroups[0].deliveryAddress.address1"
}
]
}
}
]
}
tests/po-box-in-address1.input.json{
"cart": { "deliveryGroups": [{ "deliveryAddress": { "address1": "PO Box 88", "address2": null } }] },
"validation": { "metafield": null }
}
tests/po-box-in-address1.output.json{
"operations": [
{
"validationAdd": {
"errors": [
{
"message": "We can't ship to PO boxes. Please enter a street address.",
"target": "$.cart.deliveryGroups[0].deliveryAddress.address1"
}
]
}
}
]
}
tests/post-office-box-in-address2.input.json{
"cart": {
"deliveryGroups": [{ "deliveryAddress": { "address1": "Main Post Office", "address2": "Post Office Box 1207" } }]
},
"validation": { "metafield": null }
}
tests/post-office-box-in-address2.output.json{
"operations": [
{
"validationAdd": {
"errors": [
{
"message": "We can't ship to PO boxes. Please enter a street address.",
"target": "$.cart.deliveryGroups[0].deliveryAddress.address1"
}
]
}
}
]
}
tests/street-address.input.json{
"cart": { "deliveryGroups": [{ "deliveryAddress": { "address1": "45 Harbor Street", "address2": "Unit 3" } }] },
"validation": { "metafield": null }
}
tests/street-address.output.json{ "operations": [] }
tests/street-names-like-pk.input.json{
"cart": {
"deliveryGroups": [
{
"deliveryAddress": {
"address1": "Spk Sokak No 5",
"address2": "Park Apt 3"
}
}
]
},
"validation": {
"metafield": null
}
}tests/street-names-like-pk.output.json{
"operations": []
}Written by Lintel. MIT license: free to use in your own store and in client work.
Limits and edge cases
- Checkout can attach an error to the first delivery group’s address only, so a PO box in a second shipment is reported on that same field.
- Detection is a text check; it can miss creative spellings and, rarely, flag a street name. Test with your customers’ real addresses.
- A store can activate up to 25 validation Functions.
- If you only want to stop courier rates for PO boxes, hide those options with a delivery customization instead.
Questions
- How do I stop customers from shipping to a PO box on Shopify?
- There’s no setting for it. A cart and checkout validation Function can check the address lines and block checkout with a message, as this example does.
- Does the error show on the address field?
- Yes. The error targets $.cart.deliveryGroups[0].deliveryAddress.address1, so checkout shows it under the first address line.
- Does it also block express wallets like Shop Pay or Apple Pay?
- Validation Functions run for accelerated checkouts too, according to Shopify’s reference, so the same rule applies there.
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.
