Skip to content
This guide is designed specifically for developers.

Hosted selection page

The hosted selection page is Globe’s pickup point picker on its own page, served on your store’s domain. Open it from a mobile app or a headless storefront with a Storefront API cart ID. The buyer picks a point, Globe saves it on the cart, and your app gets the result.

Set it up first in Settings → Headless and mobile apps. See Set up Globe.


https://{shop domain}/apps/appfleece-globe/select?cart={cart id}&return_url={return URL}&state={state}&locale={locale}&country={country}

{shop domain} is your store’s own storefront domain: your-store.myshopify.com or a custom domain of the online store. Shopify serves the page through Globe’s app proxy, so it works only on domains the online store answers on. If your custom domain points to a Hydrogen or other headless storefront, use the myshopify.com domain.

ParameterRequiredDescription
cartYesThe Storefront API cart ID, gid://shopify/Cart/<token>?key=<key>, URL-encoded. A bare cart token also works
return_urlNoWhere to send the buyer after they pick a point or close the picker. Must match one of your return URLs
stateNoAny value of 1 to 200 characters from A–Z a–z 0–9 . _ ~ : -. Returned unchanged with the result. Other values are ignored
localeNoen, cs, sk, ro, hu, pl or de. Without it, the page uses the browser’s Accept-Language, then English
countryNoISO 3166-1 alpha-2 delivery country, for a cart that has no delivery address yet. Otherwise the cart’s country applies

Encode the cart ID. It contains ? and =, which break the URL when sent as they are.

  1. Shows Globe’s map and list, with search, the buyer’s location and a carrier filter. It is the same picker as the cart widget, with the carriers you mapped for the delivery country.
  2. When the buyer picks a point, Globe verifies the point with the carrier’s network.
  3. Globe writes the cart attributes to the cart with the Storefront API mutation cartAttributesUpdate. Other attributes on the cart are kept.
  4. If Checkout address prefill is on in Settings → Cart Widget, Globe also sets the point’s address as the cart’s delivery address, for one use.
  5. The page reports the result in every way described below.

A return_url is allowed when it starts with one of the return URLs you added in the Globe admin:

  • the same scheme,
  • exactly the same host (subdomains do not match),
  • a path at or under the saved path.

The query and fragment of return_url are free. The page adds its own parameters.

Return URL in GlobeAllowed return_urlRefused
myapp://pickupmyapp://pickup, myapp://pickup/cz?from=cartmyapp://checkout, otherapp://pickup
https://shop.example.com/carthttps://shop.example.com/cart?step=2https://www.shop.example.com/cart, https://shop.example.com/account

You can save up to 10 return URLs. Saved URLs cannot have a query or fragment. Web addresses must use https, except http://localhost and http://127.0.0.1 for development. Custom schemes, universal links and Android App Links are all accepted.


The page sends the result every way it can, so use whichever fits your app. Handle the result once, because more than one may arrive.

With a return_url, the page navigates to it with these query parameters added:

ParameterDescription
statusselected or cancelled
point_idThe point’s ID. Only for selected
carrierThe carrier code, for example packeta. Only for selected
point_nameThe point’s name. Only for selected
stateYour state, if you sent one
myapp://pickup?status=selected&point_id=12345&carrier=packeta&point_name=Z-BOX%20Praha%201&state=c8f2

Without a return_url, the page shows a short message that the buyer can return to the app.

If the page runs in a WebView that exposes one of these objects, it calls it with the result message:

PlatformBridgeArgument
iOS (WKWebView)window.webkit.messageHandlers.globePickup.postMessage(message)The message as an object
Android (WebView)window.GlobePickupAndroid.postMessage(json), an @JavascriptInterfaceThe message as a JSON string
React Native (react-native-webview)window.ReactNativeWebView.postMessage(json)The message as a JSON string

If the page was opened with window.open or in an iframe, it posts the message to window.opener or window.parent. It posts only to the web origins of your https return URLs. Add your storefront’s address as a return URL, for example https://shop.example.com/cart, even if you never redirect to it.

{
"type": "globe:pickup-point",
"status": "selected",
"state": "c8f2",
"cartId": "gid://shopify/Cart/c1-7a2b9e?key=4f6d0c",
"point": {
"id": "12345",
"carrier": "packeta",
"carrierId": "64aaaaaaaaaaaaaaaaaaaaa1",
"name": "Z-BOX Praha 1, Karlova",
"address": "Karlova 3, 11000 Praha"
}
}
FieldDescription
typeAlways globe:pickup-point. Ignore other messages
statusselected or cancelled
stateYour state, or null
cartIdThe cart the point was saved on
pointFor selected: id, carrier (carrier code), carrierId (your Globe carrier instance), name and address. null for cancelled

The message only tells your app what happened. The cart is the source of truth. Query the cart’s attributes again, show the chosen point if you want to, and continue to the cart’s checkoutUrl.

query Cart($cartId: ID!) {
cart(id: $cartId) {
checkoutUrl
attributes { key value }
}
}

The page shows a short error in the buyer’s language when it cannot run. Nothing is written to the cart, and no result is sent.

CauseStatusWhat the buyer sees
cart is missing or not a cart ID400The link is missing the cart
The cart does not exist: it expired, became an order, or belongs to another store404This cart no longer exists
return_url does not match a return URL400This return address is not allowed for this shop
The page is turned off403Pickup point selection is not enabled for this shop
Globe has no active plan, or the storefront cart permissions were not approved402, 503Pickup point selection is temporarily unavailable

When the delivery country has no pickup point carriers, the page says so and offers a link back to return_url with status=cancelled. It sends no bridge or postMessage result in that case, so give the buyer a way to close the view.

If saving fails, for example because the carrier no longer has the point, the picker shows an alert and stays open, so the buyer can choose another point.

The cart ID gives access to the buyer’s cart. The page is served with Cache-Control: no-store and Referrer-Policy: no-referrer, so it is not cached and does not pass its URL to map tiles or other requests. Do not log the full page URL in your app or analytics.


  • JavaScript on. Android also needs DOM storage.
  • No third-party cookies are needed.
  • Catch your return_url in the WebView’s navigation callback, so the WebView does not try to load it.
  • On iOS, implement WKUIDelegate’s webView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:). WKWebView drops JavaScript alerts without it, and the page uses one when a point cannot be saved.
  • If the online store is password protected, as development stores are, the page shows Shopify’s password page first. Enter the storefront password once in the same WebView.

Location on iOS. Add NSLocationWhenInUseUsageDescription to your app’s Info.plist. WKWebView then uses the app’s location permission, and asks the buyer once per site when the page requests the location. Without the key, the page cannot get the location and the buyer searches by address instead.

Location on Android. Declare ACCESS_FINE_LOCATION in the manifest, call setGeolocationEnabled(true), and implement WebChromeClient.onGeolocationPermissionsShowPrompt. Request the runtime permission there and pass the answer to the callback.


The view controller registers the globePickup message handler and also catches the myapp://pickup redirect.

import UIKit
import WebKit
final class PickupPointViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
private let shopDomain = "your-store.myshopify.com"
private let returnURL = "myapp://pickup"
private let cartId: String
private let onFinish: (_ status: String, _ pointId: String?) -> Void
private var webView: WKWebView!
private var finished = false
init(cartId: String, onFinish: @escaping (_ status: String, _ pointId: String?) -> Void) {
self.cartId = cartId
self.onFinish = onFinish
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(self, name: "globePickup")
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.navigationDelegate = self
view.addSubview(webView)
webView.load(URLRequest(url: selectionURL()))
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
webView.configuration.userContentController.removeScriptMessageHandler(forName: "globePickup")
}
private func selectionURL() -> URL {
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-._~"))
func encode(_ value: String) -> String { value.addingPercentEncoding(withAllowedCharacters: allowed) ?? "" }
let query = "cart=\(encode(cartId))&return_url=\(encode(returnURL))&locale=cs"
return URL(string: "https://\(shopDomain)/apps/appfleece-globe/select?\(query)")!
}
// Native bridge
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == "globePickup",
let body = message.body as? [String: Any],
body["type"] as? String == "globe:pickup-point",
let status = body["status"] as? String else { return }
let point = body["point"] as? [String: Any]
finish(status: status, pointId: point?["id"] as? String)
}
// Redirect to return_url
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url, url.absoluteString.hasPrefix(returnURL) else {
decisionHandler(.allow)
return
}
decisionHandler(.cancel)
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
let status = items.first { $0.name == "status" }?.value ?? "cancelled"
finish(status: status, pointId: items.first { $0.name == "point_id" }?.value)
}
private func finish(status: String, pointId: String?) {
guard !finished else { return }
finished = true
dismiss(animated: true) { self.onFinish(status, pointId) }
}
}

In onFinish, query the cart again and continue to checkoutUrl.

Alternative: ASWebAuthenticationSession. For the redirect flow only, open the page in an authentication session with callbackURLScheme: "myapp". The completion handler receives the return_url with the result parameters, and nil when the buyer closes the sheet. iOS shows a system prompt before the sheet opens, and the page cannot reach native bridges there.

import AuthenticationServices
let session = ASWebAuthenticationSession(url: selectionURL, callbackURLScheme: "myapp") { callbackURL, _ in
guard let callbackURL else { return } // closed by the buyer
let items = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems ?? []
let status = items.first { $0.name == "status" }?.value
// Query the cart again, then continue to checkoutUrl.
}
session.presentationContextProvider = self
session.start()

The activity exposes GlobePickupAndroid, handles the location prompt and catches the myapp://pickup redirect.

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.webkit.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import org.json.JSONObject
class PickupPointActivity : AppCompatActivity() {
private val shopDomain = "your-store.myshopify.com"
private val returnUrl = "myapp://pickup"
private lateinit var webView: WebView
private var finished = false
private var geoOrigin: String? = null
private var geoCallback: GeolocationPermissions.Callback? = null
private val locationPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
geoCallback?.invoke(geoOrigin, granted, false)
geoCallback = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.setGeolocationEnabled(true)
webView.addJavascriptInterface(Bridge(), "GlobePickupAndroid")
webView.webChromeClient = object : WebChromeClient() {
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
val granted = ContextCompat.checkSelfPermission(
this@PickupPointActivity, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (granted) {
callback.invoke(origin, true, false)
} else {
geoOrigin = origin
geoCallback = callback
locationPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
if (!url.toString().startsWith(returnUrl)) return false
finishWith(url.getQueryParameter("status") ?: "cancelled", url.getQueryParameter("point_id"))
return true
}
}
val cartId = requireNotNull(intent.getStringExtra("cartId"))
val url = Uri.parse("https://$shopDomain/apps/appfleece-globe/select").buildUpon()
.appendQueryParameter("cart", cartId)
.appendQueryParameter("return_url", returnUrl)
.appendQueryParameter("locale", "cs")
.build()
webView.loadUrl(url.toString())
}
inner class Bridge {
@JavascriptInterface
fun postMessage(json: String) {
val message = JSONObject(json)
if (message.optString("type") != "globe:pickup-point") return
val pointId = message.optJSONObject("point")?.optString("id")
// JavascriptInterface methods run on a background thread.
runOnUiThread { finishWith(message.optString("status"), pointId) }
}
}
private fun finishWith(status: String, pointId: String?) {
if (finished) return
finished = true
val result = Intent().putExtra("status", status).putExtra("pointId", pointId)
setResult(if (status == "selected") RESULT_OK else RESULT_CANCELED, result)
finish()
}
}

Uri.Builder.appendQueryParameter encodes the cart ID. If your return URL is an Android App Link (https://…), match it the same way in shouldOverrideUrlLoading. The WebView does not hand App Links to your app on its own.


import React, { useRef } from 'react';
import { WebView, type WebViewMessageEvent } from 'react-native-webview';
const SHOP_DOMAIN = 'your-store.myshopify.com';
const RETURN_URL = 'myapp://pickup';
type Result = { status: 'selected' | 'cancelled'; pointId?: string };
function queryParam(url: string, name: string): string | undefined {
const match = new RegExp(`[?&]${name}=([^&#]*)`).exec(url);
return match ? decodeURIComponent(match[1]) : undefined;
}
export function PickupPointScreen({ cartId, onDone }: { cartId: string; onDone: (result: Result) => void }) {
const finished = useRef(false);
const finish = (result: Result) => {
if (finished.current) return;
finished.current = true;
onDone(result);
};
const uri =
`https://${SHOP_DOMAIN}/apps/appfleece-globe/select` +
`?cart=${encodeURIComponent(cartId)}&return_url=${encodeURIComponent(RETURN_URL)}&locale=cs`;
return (
<WebView
source={{ uri }}
javaScriptEnabled
domStorageEnabled
geolocationEnabled
// Without myapp://* here, the WebView opens the return URL itself.
originWhitelist={['https://*', 'myapp://*']}
onMessage={(event: WebViewMessageEvent) => {
let message: any;
try {
message = JSON.parse(event.nativeEvent.data);
} catch {
return;
}
if (message?.type !== 'globe:pickup-point') return;
finish({ status: message.status, pointId: message.point?.id });
}}
onShouldStartLoadWithRequest={(request) => {
if (!request.url.startsWith(RETURN_URL)) return true;
const status = queryParam(request.url, 'status') === 'selected' ? 'selected' : 'cancelled';
finish({ status, pointId: queryParam(request.url, 'point_id') });
return false;
}}
/>
);
}

geolocationEnabled applies to Android. Request ACCESS_FINE_LOCATION with PermissionsAndroid before you open the screen. On iOS, add NSLocationWhenInUseUsageDescription to Info.plist.


Open the page from a click handler, so the browser does not block the popup. Add your storefront’s origin as a return URL in Globe, for example https://shop.example.com/cart, or the page will not post the message to it.

const SHOP_ORIGIN = 'https://your-store.myshopify.com';
type PickupMessage = {
type: 'globe:pickup-point';
status: 'selected' | 'cancelled';
state: string | null;
cartId: string;
point: { id: string; carrier: string | null; carrierId: string | null; name: string; address: string } | null;
};
export function choosePickupPoint(cartId: string): Promise<PickupMessage> {
const state = crypto.randomUUID();
const url = new URL('/apps/appfleece-globe/select', SHOP_ORIGIN);
url.searchParams.set('cart', cartId);
url.searchParams.set('state', state);
const popup = window.open(url, 'globe-pickup', 'popup,width=480,height=760');
if (!popup) return Promise.reject(new Error('The popup was blocked'));
return new Promise((resolve) => {
const closedCheck = window.setInterval(() => {
if (popup.closed) done({ type: 'globe:pickup-point', status: 'cancelled', state, cartId, point: null });
}, 500);
function onMessage(event: MessageEvent) {
if (event.origin !== SHOP_ORIGIN) return;
const data = event.data as PickupMessage;
if (data?.type !== 'globe:pickup-point' || data.state !== state) return;
done(data);
}
function done(message: PickupMessage) {
window.clearInterval(closedCheck);
window.removeEventListener('message', onMessage);
if (!popup.closed) popup.close();
resolve(message);
}
window.addEventListener('message', onMessage);
});
}

event.origin is the domain you opened the page on. Check it exactly, then check type and your state.

After the promise resolves, load the cart again. In Hydrogen, revalidate the route that reads the cart:

import { useRevalidator } from 'react-router';
export function PickupButton({ cartId }: { cartId: string }) {
const revalidator = useRevalidator();
return (
<button
type="button"
onClick={async () => {
const result = await choosePickupPoint(cartId);
if (result.status === 'selected') revalidator.revalidate();
}}
>
Choose a pickup point
</button>
);
}

The page also works in an iframe and posts to window.parent with the same rules. Some stores send frame-ancestors 'none' or X-Frame-Options: DENY on their storefront domain, and then the browser refuses to show the page in a frame. If the frame stays empty, use the popup or the redirect.

Send the buyer to the page with a return_url on your storefront, then read the result when they come back:

const url = new URL('/apps/appfleece-globe/select', SHOP_ORIGIN);
url.searchParams.set('cart', cart.id);
url.searchParams.set('return_url', 'https://shop.example.com/cart');
window.location.assign(url);

The buyer returns to https://shop.example.com/cart?status=selected&point_id=…. The cart route’s loader reads the cart again anyway, so the point is in its attributes.