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 →
When to enable
Section titled “When to enable”| 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.
Enable in the theme editor
Section titled “Enable in the theme editor”- Online Store → Themes → Customize
- App embeds → HK Pickup
- Turn on Developer mode
- Save
What HK Pickup still does
Section titled “What HK Pickup still does”| 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.
Imperative API (window.hkpickup)
Section titled “Imperative API (window.hkpickup)”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 |
Events
Section titled “Events”All events are document CustomEvents with prefix hkpickup:.
Register listeners after hkpickup:ready (or check window.hkpickup?.isReady).
Event reference
Section titled “Event reference”| 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.
store object
Section titled “store object”{ "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.
cartAttributes keys
Section titled “cartAttributes keys”| 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.
Cancelable close (hkpickup:close)
Section titled “Cancelable close (hkpickup:close)”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.
Override a hardcoded checkout URL
Section titled “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'// orwindow.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.
Put the address back on the URL
Section titled “Put the address back on the URL”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 support
Section titled “Browser support”| 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.
Province on the Hong Kong form
Section titled “Province on the Hong Kong form”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.
Debug — log all events
Section titled “Debug — log all events”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)' : '') ))Testing
Section titled “Testing”After wiring checkout, run the same checks in Verify checkout.
Related
Section titled “Related”- Theme editor setup — Developer mode checkbox
- Troubleshooting — dev mode FAQ