Skip to content

Developer mode — Shopify checkout integration API

Use Developer mode when another app or custom checkout script conflicts with HK Pickup’s automatic checkout intercept, or when you need full control over when the delivery dialog opens and how checkout continues.

Install HK Pickup on the Shopify App Store →


Situation Developer mode
Single HK Pickup install, default checkout Off (recommended)
Another app intercepts checkout On — you wire hkpickup.open() yourself
Custom checkout / headless flow On — listen for hkpickup:* events

With Developer mode on, HK Pickup does not bind checkout buttons. Checkout clicks go to Shopify unless you add custom JavaScript.

Need custom “who sees the dialog” logic with your own JavaScript? Use Developer mode. For normal live checkout while migrating from another pickup app, use Pilot mode instead — no custom code required.

Some other apps own the checkout button and always open a hardcoded /checkout of their own. They do not use HK Pickup’s address URL. Default mode cannot intercept that click. Keep developer mode on and override the address on the URL they open.


  1. Online Store → Themes → Customize
  2. App embeds → HK Pickup
  3. Turn on Developer mode
  4. Save

Responsibility Owner
Show delivery dialog UI HK Pickup (when you call open())
Write cart attributes (_sf_delivery_mode, _sf_pickup_*) HK Pickup — before home / confirm events
Navigate to checkout / submit cart form You (integrator)

If /cart/update.js fails, HK Pickup shows an error in the dialog and does not fire home or confirm events.


Available only when Developer mode is on.

Member Description
isReady true after init and storefront config load
open() Reset to step 1, show dialog, fire hkpickup:open
close(options?) { force?: boolean } — closes dialog; fires non-cancelable hkpickup:close with reason: 'api'. Use force: true after async work when you previously blocked a home/confirm close

All events are document CustomEvents with prefix hkpickup:.

Register listeners after hkpickup:ready (or check window.hkpickup?.isReady).

Event When detail
hkpickup:ready Init complete { developerMode: true, locale }
hkpickup:open Dialog opened via open() { source: 'api' }
hkpickup:close Before dialog closes (dev mode) { reason } — see cancelable rules below
hkpickup:home Home delivery chosen (attrs cleared) { cartAttributes }
hkpickup:back Back from SF list → step 1 {}
hkpickup:location-select SF row selected { store }
hkpickup:confirm SF Confirm (attrs saved) { store, checkoutUrl, cartAttributes }

Not fired: step-1 “SF Express pickup” (internal only), filter/search changes.

{
"code": "H852K067P",
"type": "store",
"category": "store",
"name": "SF Station …",
"address": "…",
"area": "HK",
"region": "Hong Kong Island",
"city": "Central",
"district": "Central",
"province_code": "HK"
}

area is HK or MO. province_code is Shopify’s Hong Kong region code (HK, KL or NT), or null for Macau or when the district is unknown.

Key Home Pickup
_sf_delivery_mode home pickup
_sf_pickup_code "" SF code
_sf_pickup_type "" Service type
_sf_pickup_address "" Address
_sf_pickup_region "" Region
_sf_pickup_district "" District
_sf_pickup_locale "" zh-HK / zh-CN / en-US

If the merchant unticks Save SF Express details on the order in Admin settings, every key above is "" for both home and pickup, and HK Pickup writes those blank values to the cart. Use detail.store for the pickup point instead.


Only home and confirm close reasons are cancelable. Listeners may call event.preventDefault() synchronously to keep the dialog open (e.g. while another plugin runs).

detail.reason Cancelable? Behavior
home Yes After hkpickup:home
confirm Yes After hkpickup:confirm
dismiss No X button or backdrop — always closes
api No hkpickup.close() — always closes

Async pattern:

document.addEventListener('hkpickup:close', (e) => {
if (e.detail.reason !== 'confirm') return
e.preventDefault()
runOtherPlugin().finally(() => window.hkpickup.close({ force: true }))
})

Event order (home / confirm): cart attrs saved → hkpickup:home or hkpickup:confirm → cancelable hkpickup:close → close unless prevented.


Dawn example — intercept checkout yourself

Section titled “Dawn example — intercept checkout yourself”

Add to theme custom JavaScript (or your app’s asset):

document.addEventListener('hkpickup:ready', () => {
document.querySelectorAll('button[name="checkout"]').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault()
window.hkpickup.open()
})
})
})
document.addEventListener('hkpickup:home', () => {
window.location.href = '/checkout'
})
document.addEventListener('hkpickup:confirm', (e) => {
window.location.href = e.detail.checkoutUrl
})

Adjust selectors for your theme if checkout buttons differ.

Use that confirm handler when your script is the one that opens checkout. detail.checkoutUrl already includes the pickup address. When another app owns the button and hardcodes /checkout, you cannot send the shopper there yourself. See Override a hardcoded checkout URL.


Use this when default mode cannot run. Another app has taken the checkout button and always finishes by opening a hardcoded path:

window.location.href = '/checkout'
// or
window.location.replace('/checkout')

That assignment is often inside a closed-over function, so you cannot change where that app sends the shopper. Leave its navigation in place, and override the address on the URL it opens.

The pickup dialog still completes, and the shopper still reaches checkout, but the shipping address fields are empty. The pickup choice itself is saved. Cart attributes such as _sf_pickup_code and _sf_pickup_address are on the cart and later on the order. Those attributes do not fill the checkout form. Shopify prefills that form from query parameters on the checkout URL:

  • checkout[shipping_address][first_name], checkout[shipping_address][last_name] (signed-in customers only)
  • checkout[shipping_address][address1]
  • checkout[shipping_address][address2]
  • checkout[shipping_address][city]
  • checkout[shipping_address][province]
  • checkout[shipping_address][country]

HK Pickup builds that URL and exposes it on the confirm event as event.detail.checkoutUrl. In developer mode it does not navigate. It updates the cart, dispatches hkpickup:confirm or hkpickup:home, and closes.

A hardcoded /checkout has none of those parameters, so the URL HK Pickup already built is ignored. A direct location.href assignment also does not submit the cart form, which is the only other way hidden checkout[shipping_address][...] inputs would be included. POST /cart/update.js can update the note, attributes, and line quantities. It cannot set the checkout shipping address.

The address has to be merged back onto the URL at the moment that hardcoded navigation happens.

Keep the query string from detail.checkoutUrl. Save it on hkpickup:confirm. Clear it on home delivery when detail.checkoutUrl is absent, so an earlier pickup cannot leak into a home-delivery checkout.

Listen for the Navigation API. When the navigation is a same-origin request to /checkout or /checkout/, it can be cancelled, and it does not already contain checkout[shipping_address][address1], cancel it and assign location.href to the same URL with the stored address parameters added. Leave any existing query parameters in place.

let address = null
document.addEventListener('hkpickup:confirm', (event) => {
const checkoutUrl = event.detail.checkoutUrl
address = checkoutUrl
? new URL(checkoutUrl, location.origin).searchParams
: null
})
document.addEventListener('hkpickup:home', (event) => {
if (!event.detail.checkoutUrl) address = null
})
if (window.navigation) {
navigation.addEventListener('navigate', (event) => {
if (!address || !event.cancelable) return
const url = new URL(event.destination.url)
if (url.origin !== location.origin || !/\/checkout\/?$/.test(url.pathname)) return
if (url.searchParams.has('checkout[shipping_address][address1]')) return
address.forEach((value, name) => {
url.searchParams.set(name, value)
})
event.preventDefault()
location.href = url.toString()
})
}

Guard the listener with if (window.navigation). Where the API is missing, the dialog and the checkout redirect still run. Only the address prefill is skipped.

Browser Version Released
Chrome, Edge 102 24 May 2022
Safari, iOS 26.2 12 December 2025
Firefox 147 13 January 2026

Safari before 26.2 and Firefox before 147 cannot intercept location.href. A possible fallback is to assign location.href again to the address URL after the bare /checkout assignment. The last assignment wins, but only if it happens after the first one. Assigning too early is overwritten by /checkout.

Shopify’s Hong Kong checkout only accepts 香港島, 九龍 or 新界 as the region, and replaces anything else with the first option. HK Pickup sends the matching code (HK, KL or NT) from store.province_code, so a New Territories pickup shows 新界. When the district can’t be mapped, no province is sent. Macau points send the SF region text unchanged. Keep these parameters when you rebuild the URL yourself.

Shop Pay rejects a prefilled address with no recipient name, so first_name / last_name are included when the customer is signed in.


Paste in DevTools console, then reload:

;['ready','open','close','home','back','location-select','confirm'].forEach((n) =>
document.addEventListener(`hkpickup:${n}`, (e) =>
console.log(`hkpickup:${n}`, e.detail, e.cancelable ? '(cancelable)' : '')
)
)

After wiring checkout, run the same checks in Verify checkout.