Car Battery Pro
Appendix

Postcode capabilities

How delivery and installation availability is resolved from postcode_capabilities metaobjects.

Postcode capabilities

๐Ÿ›  Dev ยท ๐Ÿ‘ค Ops (reference)

When a customer enters a postcode, what they're allowed to do in that area - delivery and/or at-home installation - is driven by postcode_capabilities metaobjects, matched by postcode. See Delivery zones for the customer-facing behaviour.

How it resolves

  1. The theme sends the postcode to the backend (/v1/postcode/{postcode}).
  2. The backend matches it against the postcode_capabilities metaobjects and enriches the response with that area's capabilities.
  3. The browser receives the result and stores it. Crucially, the storefront just gets delivery and installation_service as booleans (plus the suburb/state and installer calendar URL) - all the matching logic lives server-side. See The backend API.

Because the matching and enriching happen in the backend, the theme never decides eligibility itself - it only reads the booleans it's handed and updates the UI (delivery messaging, installation CTA).

The postcode_capabilities metaobject

Managed under Shopify Admin โ†’ Content โ†’ Metaobjects โ†’ Postcode capabilities. To change what an area can do, edit or add an entry - no code change required. See Metaobjects.

Field (key)NameTypeMeaning
display_nameDisplay NameText (single line)Friendly location name shown to the customer.
suburb_nameSuburb NameText (single line)The suburb for this entry.
postcodePostcodeText (single line)Comma-separated postcode patterns this entry matches - see Defining ranges.
installation_serviceInstallation ServiceTrue/falseWhether at-home installation is available in this area.
deliveryDeliveryTrue/falseWhether delivery is available in this area.
installer_calendar_urlInstaller calendar IDsText (list)The installer calendar ID(s) used for this area's bookings.

Range patterns require the postcode field to be a single-line text field. If the definition still has it as Number (integer), it will reject values like 3000,30* - and Shopify cannot change a field's type in place. Migrate by adding a new text field (or deleting and re-creating postcode, which discards its existing values) before bulk-loading patterns.

This is now the source of truth for delivery and installation availability - not a hard-coded list. To open or close an area, update its postcode_capabilities entry in admin (or have the backend team confirm the entry exists for that postcode).

Defining ranges with commas and wildcards

The postcode field holds a comma-separated list of patterns. The backend splits the value on commas and scans the entire list of entries for matches. A trailing * is a prefix wildcard: 30* matches any postcode starting with 30.

Example valueMatches
3000Exactly 3000.
3000,3004,3006Any of the three listed postcodes.
30*3000โ€“3099.
30*,31*3000โ€“3199.
3*3000โ€“3999 (all of VIC).
3000,32*,39773000, anything in 3200โ€“3299, and 3977.

Avoid overlapping entries. Because every entry is scanned, a postcode can match more than one - e.g. an entry with 30* and another with 3000. Which entry wins is decided by the backend's precedence rule, so either keep patterns mutually exclusive or confirm the intended precedence (e.g. exact match beats wildcard) with the backend team before relying on overlaps.

Bulk-creating entries with GraphQL

For loading many areas at once, use the Admin API's bulk mutation pipeline instead of creating entries one by one in admin. Requires an Admin API token with the write_metaobjects scope.

Prefer the import tool. We provide a small web app at https://cbp-postcode-importer.tools.croproai.com/ that runs this exact procedure for you: drop a JSONL file and it validates every line (pattern syntax, handles, booleans, overlapping ranges) before staging, uploading, running the bulk mutation, and reporting per-line results. It also provides a downloadable sample JSONL to start from.

Build a JSONL file

One line per entry. Every field value is a string: booleans as "true"/"false", the calendar ID list as a JSON-encoded array string. Handles must be unique - with ranges, entries describe zones rather than single suburbs, so name them accordingly:

postcode_capabilities.jsonl
{"handle": {"type": "postcode_capabilities", "handle": "melbourne-metro"}, "metaobject": {"fields": [{"key": "display_name", "value": "Melbourne Metro"}, {"key": "suburb_name", "value": "Melbourne"}, {"key": "postcode", "value": "3000,3004,30*,31*"}, {"key": "installation_service", "value": "true"}, {"key": "delivery", "value": "true"}, {"key": "installer_calendar_url", "value": "[\"cal-id-melb-1\",\"cal-id-melb-2\"]"}]}}
{"handle": {"type": "postcode_capabilities", "handle": "mornington-peninsula"}, "metaobject": {"fields": [{"key": "display_name", "value": "Mornington Peninsula"}, {"key": "suburb_name", "value": "Mornington"}, {"key": "postcode", "value": "3915,3930,3931,393*,394*"}, {"key": "installation_service", "value": "false"}, {"key": "delivery", "value": "true"}, {"key": "installer_calendar_url", "value": "[]"}]}}

Stage the upload

mutation StageBulkUpload {
  stagedUploadsCreate(
    input: [
      {
        resource: BULK_MUTATION_VARIABLES
        filename: "postcode_capabilities.jsonl"
        mimeType: "text/jsonl"
        httpMethod: POST
      }
    ]
  ) {
    stagedTargets {
      url
      resourceUrl
      parameters {
        name
        value
      }
    }
    userErrors {
      field
      message
    }
  }
}

The response's stagedTargets[0] gives you a url to POST to and a parameters array. Those parameters are name/value pairs Shopify signs for its storage backend (Google Cloud Storage, for bulk variables). Treat them as opaque: don't hardcode, drop, or reorder them. Replay each one as its own multipart form field, in the order returned, then add the file last (Google Cloud Storage rejects the upload if file isn't the final field).

The exact set varies by API version, so always read them from the response rather than assuming. Typical pairs you'll see:

Parameter (name)What its value is
keyThe object path in storage. This is also the value you pass as stagedUploadPath in the next step.
Content-Type / mime_typeMust match the file โ€” text/jsonl.
success_action_statusThe HTTP status the storage returns on success (e.g. 201).
aclObject access control (e.g. private).
policyA base64-encoded, signed upload policy.
x-goog-algorithm, x-goog-credential, x-goog-date, x-goog-signatureGoogle Cloud Storage signature fields authorising this one POST.

Rather than copy them by hand, build the -F flags straight from the response (saved here as staged.json):

url=$(jq -r '.data.stagedUploadsCreate.stagedTargets[0].url' staged.json)

# One -F per returned parameter, preserving order:
mapfile -t fields < <(jq -r \
  '.data.stagedUploadsCreate.stagedTargets[0].parameters[] | "\(.name)=\(.value)"' staged.json)
args=(); for f in "${fields[@]}"; do args+=(-F "$f"); done

# Parameters first, file LAST:
curl -X POST "$url" "${args[@]}" -F "file=@postcode_capabilities.jsonl"

The stagedUploadPath for the next step is the key parameter's value:

jq -r '.data.stagedUploadsCreate.stagedTargets[0].parameters[]
  | select(.name == "key") | .value' staged.json

Run the bulk mutation

Uses metaobjectUpsert keyed on handle, so re-running the same file updates entries instead of duplicating them - safe for corrections.

mutation BulkCreatePostcodeCapabilities {
  bulkOperationRunMutation(
    mutation: """
    mutation CreateEntry($handle: MetaobjectHandleInput!, $metaobject: MetaobjectUpsertInput!) {
      metaobjectUpsert(handle: $handle, metaobject: $metaobject) {
        metaobject { id handle }
        userErrors { field message code }
      }
    }
    """
    stagedUploadPath: "<key-parameter-value-from-previous-step>"
  ) {
    bulkOperation {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}

Poll until complete

query BulkStatus {
  currentBulkOperation(type: MUTATION) {
    id
    status
    objectCount
    errorCode
    url
    partialDataUrl
  }
}

When status is COMPLETED, download the url - a JSONL of per-line results. Check each line's userErrors: the bulk operation reports success even when individual upserts failed (e.g. a pattern value rejected by a still-integer postcode field).

Notes:

  • Only one bulk mutation runs at a time per shop; the JSONL file is capped at 20ย MB.
  • To verify afterwards, list the entries: metaobjects(type: "postcode_capabilities", first: 50).

On this page