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

Page: https://lintelagency.com/resources/shopify-functions/block-po-box-addresses

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

## Facts

- **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:** 2 November 2026

## How to use it

1. Create an app with the Shopify CLI, then add the extension: shopify app generate extension --template cart_checkout_validation, with JavaScript.
2. Replace the generated shopify.extension.toml and the two files in src/ with the files below.
3. Run the fixtures with shopify app function run --input tests/po-box-in-address1.input.json.
4. Deploy with shopify app deploy and turn the validation on with validationCreate.
5. Optionally set your own message in the validation’s $app:function-configuration metafield: {"message": "…"}.

## Code

### shopify.extension.toml

```toml
api_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.graphql

```graphql
query CartValidationsGenerateRunInput {
  cart {
    deliveryGroups {
      deliveryAddress {
        address1
        address2
      }
    }
  }
  validation {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
```

### src/cart_validations_generate_run.js

```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

```json
{
  "cart": {
    "deliveryGroups": [
      {
        "deliveryAddress": {
          "address1": "Kadıköy PK 34",
          "address2": null
        }
      }
    ]
  },
  "validation": {
    "metafield": null
  }
}
```

### tests/po-box-after-district.output.json

```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

```json
{
  "cart": { "deliveryGroups": [{ "deliveryAddress": { "address1": "PO Box 88", "address2": null } }] },
  "validation": { "metafield": null }
}
```

### tests/po-box-in-address1.output.json

```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

```json
{
  "cart": {
    "deliveryGroups": [{ "deliveryAddress": { "address1": "Main Post Office", "address2": "Post Office Box 1207" } }]
  },
  "validation": { "metafield": null }
}
```

### tests/post-office-box-in-address2.output.json

```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

```json
{
  "cart": { "deliveryGroups": [{ "deliveryAddress": { "address1": "45 Harbor Street", "address2": "Unit 3" } }] },
  "validation": { "metafield": null }
}
```

### tests/street-address.output.json

```json
{ "operations": [] }
```

### tests/street-names-like-pk.input.json

```json
{
  "cart": {
    "deliveryGroups": [
      {
        "deliveryAddress": {
          "address1": "Spk Sokak No 5",
          "address2": "Park Apt 3"
        }
      }
    ]
  },
  "validation": {
    "metafield": null
  }
}
```

### tests/street-names-like-pk.output.json

```json
{
  "operations": []
}
```

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

- [Cart and Checkout Validation Function API](https://shopify.dev/docs/api/functions/latest/cart-and-checkout-validation)

## Related

- [Activate a delivery, payment or validation Function](https://lintelagency.com/resources/graphql/register-function-customization)
- [Hide courier delivery options for PO box addresses](https://lintelagency.com/resources/shopify-functions/hide-delivery-options-for-po-boxes)
- [Blocking a Shopify checkout that breaks your rules, with Cart and Checkout Validation](https://lintelagency.com/journal/shopify-checkout-validation)
- [Checkout UI extensions vs Shopify Functions](https://lintelagency.com/journal/checkout-ui-extensions-vs-shopify-functions)

License: MIT. Free to use in your own store and in client work.
