Ir al contenido
This guide is designed specifically for developers.

Storefront pickup API

Esta página aún no está disponible en tu idioma.

The Storefront pickup API finds a store’s pickup points by place, before the order exists. Call it from a native app or a headless storefront to build your own pickup point picker, then write the chosen point to the Storefront API cart.

  • Base URL: https://api-globe.appfleece.app/v1
  • Endpoints: GET /storefront/carriers, GET /storefront/pickup-points
  • Errors: the same error body as the Globe API

To use Globe’s own picker instead, see Hosted selection page. For the bigger picture, see Pickup points in headless storefronts and mobile apps.


Create a publishable key in Settings → Headless and mobile apps → Storefront pickup API in the Globe admin.

  • A publishable key looks like glb_pk_…. It belongs to one store, and the store comes from the key.
  • It only searches your carriers’ pickup points. It is safe to ship inside a mobile app or a browser bundle.
  • A store has one publishable key. You can show and copy it again at any time.
  • Rotate key creates a new key, and the old key stops working at once. Ship the new key in your app before you rotate.
  • Revoke key stops the key at once. Uninstalling Globe also revokes it.

Send the key in a header:

X-Globe-Publishable-Key: glb_pk_…

Authorization: Bearer glb_pk_… works too. A key in the query string (api_key, publishable_key or key) is refused with 400 validation_error.

Keys are not interchangeable:

  • A secret API key (glb_live_…) sent to a storefront endpoint gets 401 invalid_token. A secret key in an app or a browser is a leaked key. Revoke it in Settings → API.
  • A publishable key sent to any other Globe API endpoint gets 401 invalid_token.

The store needs an active Globe plan. Otherwise requests get 403 plan_required.

  • Requests without an Origin header, from native apps and servers, are always allowed.
  • With no origins listed under Allowed web origins, every website may call the API, and responses carry Access-Control-Allow-Origin: *.
  • With origins listed, only those websites get CORS headers. Other websites get 403 origin_not_allowed without CORS headers, so the browser hides the response from the page.
  • Preflight OPTIONS requests answer 204 and are cached for 10 minutes.
BucketLimit
storefront_ip_per_minute60 requests per minute per key and client IP
storefront_key_per_minute600 requests per minute per key

Over a limit, the API answers 429 rate_limited with Retry-After and the X-RateLimit-* headers of the refusing bucket. Support can change the limits of a store.

Call the API from the buyer’s device or browser. A server that calls it for all buyers uses one IP bucket for all of them.

Search when the buyer stops typing or moving the map, not on every keystroke or frame.

Responses are JSON with snake_case fields and Cache-Control: no-store, private. Every response carries X-Request-Id. Send it to support with any question about a call. Lists return {data, next_cursor, has_more}. These endpoints are not paginated, so next_cursor is always null.


GET /v1/storefront/carriers?country=CZ

Returns the carriers you offer pickup points of in a country, with the point types each offers. Use it to build a carrier or type filter.

ParameterRequiredDescription
countryYesThe delivery country, ISO 3166-1 alpha-2
{
"data": [
{
"id": "64aaaaaaaaaaaaaaaaaaaaa1",
"code": "gls",
"name": "GLS Výdejní místo",
"icon": null,
"types": [
{ "code": "parcelshop", "name": "ParcelShop" }
]
}
],
"next_cursor": null,
"has_more": false
}
FieldDescription
idYour Globe carrier instance. One carrier can have several instances, for example two accounts
codeThe carrier code
nameThe carrier’s name as buyers see it in this country
iconThe carrier’s icon URL, or null
typesThe point types this instance offers: code and name. Pass a code as type to the search
GET /v1/storefront/pickup-points

Returns your pickup points nearest to a place, nearest first. Only your carriers that serve country are searched.

ParameterRequiredDescription
countryYesThe delivery country, ISO 3166-1 alpha-2
latitude, longitudeOne of the two placesSearch around these coordinates
postal_codeOne of the two placesSearch around this postal code when no coordinates are sent. 2 to 16 letters, digits, spaces or hyphens
cityNoNarrows a postal_code search. At most 80 characters
carrierNoComma-separated carrier instance IDs or carrier codes from List carriers. At most 20
typeNoComma-separated point type codes. At most 20
limitNo1 to 50. Default 20

Prefer coordinates. Send the device’s location or the centre of your map. The native geocoders on iOS and Android are free to use. A postal_code search is geocoded by Globe within a small daily budget per store, so it can stop finding points on a busy day.

Terminal window
curl "https://api-globe.appfleece.app/v1/storefront/pickup-points?country=CZ&latitude=50.087&longitude=14.421&limit=10" \
-H "X-Globe-Publishable-Key: $GLOBE_PUBLISHABLE_KEY"
{
"data": [
{
"id": "2080-CZ12345",
"carrier": "gls",
"carrier_name": "GLS Výdejní místo",
"name": "Trafika U Nádraží",
"type": "parcelshop",
"address": {
"street": "Nádražní",
"street_number": "1",
"city": "Praha",
"postal_code": "11000",
"country": "CZ"
},
"formatted_address": "Nádražní 1, 110 00 Praha",
"latitude": 50.09,
"longitude": 14.43,
"distance_km": 0.36,
"url": null,
"carrier_id": "64aaaaaaaaaaaaaaaaaaaaa1",
"opening_hours": [{ "day": 1, "open": "08:00", "close": "18:00" }],
"cart_attributes": {
"globe_PickupPointId": "2080-CZ12345",
"globe_PickupPointName": "Trafika U Nádraží",
"globe_PickupPointAddress": "Nádražní 1, 110 00 Praha",
"globe_PickupPointData": "{…}",
"globe_DeliveryMethod": "pickup",
"globe_PickupPointCustomerId": ""
}
}
],
"next_cursor": null,
"has_more": false
}
FieldDescription
idThe point’s ID at the carrier
carrier, carrier_nameThe carrier code and its name as buyers see it
name, typeThe point’s name and type
addressstreet, street_number, city, postal_code, country
formatted_addressThe address on one line
latitude, longitudeThe point’s coordinates
distance_kmDistance from the searched place
urlThe point’s page at the carrier, or null
carrier_idYour carrier instance that serves the point, the id from List carriers
opening_hoursThe carrier’s opening hours as Globe stores them. The shape varies by carrier. null when unknown
cart_attributesThe cart attributes that select this point. Write them to the cart as they are

The searched place is never returned, only each point’s distance from it.

Points that need a customer number typed in at selection are left out: DHL Packstation in Germany and Luxembourg, and PPL ParcelBox in Germany. Offer them through the hosted selection page, which asks for the number.

A country with none of your carriers returns an empty list.


When the buyer picks a point, write its cart_attributes to the cart with the Storefront API. Write all of them, exactly as returned. For carriers Globe queries live, such as GLS or DHL, Globe cannot verify a point from its ID alone, and the point is then not saved on the order.

cartAttributesUpdate replaces the cart’s whole attribute list. Read the current attributes, drop the Globe pickup keys, add the point’s cart_attributes (skip empty values) and write the full list. Other apps’ attributes stay on the cart.

query CartAttributes($cartId: ID!) {
cart(id: $cartId) {
attributes { key value }
}
}
mutation CartAttributesUpdate($cartId: ID!, $attributes: [AttributeInput!]!) {
cartAttributesUpdate(cartId: $cartId, attributes: $attributes) {
cart { id checkoutUrl attributes { key value } }
userErrors { field message }
}
}

The Globe pickup keys to drop before you add the new point:

globe_PickupPointId
globe_PickupPointName
globe_PickupPointAddress
globe_PickupPointData
globe_DeliveryMethod
globe_PickupPointCustomerId
globe_PickupPointProviderShopId
globe_PickupPointCarrier

For home delivery, drop the same keys and add globe_DeliveryMethod: home. See Home delivery.

Then send the buyer to the cart’s checkoutUrl. Globe verifies the point when the order is created. See When the order is created.


The examples search around coordinates and write the chosen point to the cart. Replace your-store.myshopify.com, the Storefront API access token and the publishable key with your own.

Search, take the first point’s cart_attributes, and write them next to an attribute that was already on the cart (gift_note). The example uses jq to build the request.

Terminal window
POINT=$(curl -s "https://api-globe.appfleece.app/v1/storefront/pickup-points?country=CZ&latitude=50.087&longitude=14.421&limit=1" \
-H "X-Globe-Publishable-Key: $GLOBE_PUBLISHABLE_KEY" | jq '.data[0].cart_attributes')
jq -n --arg cartId "$CART_ID" --argjson point "$POINT" '{
query: "mutation($cartId: ID!, $attributes: [AttributeInput!]!) { cartAttributesUpdate(cartId: $cartId, attributes: $attributes) { cart { checkoutUrl } userErrors { message } } }",
variables: {
cartId: $cartId,
attributes: ([{ key: "gift_note", value: "Happy birthday" }] + ($point | to_entries | map(select(.value != ""))))
}
}' | curl -s -X POST "https://your-store.myshopify.com/api/2026-10/graphql.json" \
-H "Content-Type: application/json" \
-H "X-Shopify-Storefront-Access-Token: $STOREFRONT_TOKEN" \
-d @-
import Foundation
let globePublishableKey = "glb_pk_…"
let storefrontToken = "…"
let storefrontURL = URL(string: "https://your-store.myshopify.com/api/2026-10/graphql.json")!
let pickupKeys: Set<String> = [
"globe_PickupPointId", "globe_PickupPointName", "globe_PickupPointAddress", "globe_PickupPointData",
"globe_DeliveryMethod", "globe_PickupPointCustomerId", "globe_PickupPointProviderShopId", "globe_PickupPointCarrier",
]
struct PickupPoint: Decodable {
let id: String
let carrierName: String?
let name: String?
let formattedAddress: String?
let distanceKm: Double?
let cartAttributes: [String: String]
enum CodingKeys: String, CodingKey {
case id, name
case carrierName = "carrier_name"
case formattedAddress = "formatted_address"
case distanceKm = "distance_km"
case cartAttributes = "cart_attributes"
}
}
private struct PickupPointList: Decodable { let data: [PickupPoint] }
func searchPickupPoints(country: String, latitude: Double, longitude: Double) async throws -> [PickupPoint] {
var components = URLComponents(string: "https://api-globe.appfleece.app/v1/storefront/pickup-points")!
components.queryItems = [
URLQueryItem(name: "country", value: country),
URLQueryItem(name: "latitude", value: String(latitude)),
URLQueryItem(name: "longitude", value: String(longitude)),
]
var request = URLRequest(url: components.url!)
request.setValue(globePublishableKey, forHTTPHeaderField: "X-Globe-Publishable-Key")
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.badServerResponse) }
return try JSONDecoder().decode(PickupPointList.self, from: data).data
}
func storefront(_ query: String, _ variables: [String: Any]) async throws -> [String: Any] {
var request = URLRequest(url: storefrontURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(storefrontToken, forHTTPHeaderField: "X-Shopify-Storefront-Access-Token")
request.httpBody = try JSONSerialization.data(withJSONObject: ["query": query, "variables": variables])
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
return json?["data"] as? [String: Any] ?? [:]
}
func savePickupPoint(_ point: PickupPoint, cartId: String) async throws {
let read = try await storefront(
"query($cartId: ID!) { cart(id: $cartId) { attributes { key value } } }",
["cartId": cartId]
)
let existing = (read["cart"] as? [String: Any])?["attributes"] as? [[String: Any]] ?? []
var attributes: [[String: String]] = existing.compactMap { attribute in
guard let key = attribute["key"] as? String, !pickupKeys.contains(key) else { return nil }
return ["key": key, "value": attribute["value"] as? String ?? ""]
}
for (key, value) in point.cartAttributes where !value.isEmpty {
attributes.append(["key": key, "value": value])
}
let result = try await storefront(
"mutation($cartId: ID!, $attributes: [AttributeInput!]!) { cartAttributesUpdate(cartId: $cartId, attributes: $attributes) { userErrors { message } } }",
["cartId": cartId, "attributes": attributes]
)
let errors = (result["cartAttributesUpdate"] as? [String: Any])?["userErrors"] as? [[String: Any]] ?? []
if let message = errors.first?["message"] as? String { throw NSError(domain: "Cart", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) }
}

Use CLLocationManager for the buyer’s location, or CLGeocoder to turn a typed address into coordinates.

Run these functions off the main thread, for example in Dispatchers.IO.

import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
const val GLOBE_PUBLISHABLE_KEY = "glb_pk_…"
const val STOREFRONT_TOKEN = "…"
const val STOREFRONT_URL = "https://your-store.myshopify.com/api/2026-10/graphql.json"
val PICKUP_KEYS = setOf(
"globe_PickupPointId", "globe_PickupPointName", "globe_PickupPointAddress", "globe_PickupPointData",
"globe_DeliveryMethod", "globe_PickupPointCustomerId", "globe_PickupPointProviderShopId", "globe_PickupPointCarrier",
)
val client = OkHttpClient()
fun searchPickupPoints(country: String, latitude: Double, longitude: Double): JSONArray {
val url = "https://api-globe.appfleece.app/v1/storefront/pickup-points".toHttpUrl().newBuilder()
.addQueryParameter("country", country)
.addQueryParameter("latitude", latitude.toString())
.addQueryParameter("longitude", longitude.toString())
.build()
val request = Request.Builder().url(url).header("X-Globe-Publishable-Key", GLOBE_PUBLISHABLE_KEY).build()
client.newCall(request).execute().use { response ->
val body = response.body?.string().orEmpty()
if (!response.isSuccessful) throw IOException("Globe ${response.code}: $body")
return JSONObject(body).getJSONArray("data")
}
}
fun storefront(query: String, variables: JSONObject): JSONObject {
val payload = JSONObject().put("query", query).put("variables", variables).toString()
val request = Request.Builder()
.url(STOREFRONT_URL)
.header("X-Shopify-Storefront-Access-Token", STOREFRONT_TOKEN)
.post(payload.toRequestBody("application/json".toMediaType()))
.build()
client.newCall(request).execute().use { response ->
return JSONObject(response.body?.string().orEmpty()).getJSONObject("data")
}
}
fun savePickupPoint(point: JSONObject, cartId: String) {
val read = storefront(
"query(\$cartId: ID!) { cart(id: \$cartId) { attributes { key value } } }",
JSONObject().put("cartId", cartId),
)
val existing = read.getJSONObject("cart").getJSONArray("attributes")
val attributes = JSONArray()
for (i in 0 until existing.length()) {
val attribute = existing.getJSONObject(i)
val key = attribute.getString("key")
if (key in PICKUP_KEYS) continue
val value = if (attribute.isNull("value")) "" else attribute.getString("value")
attributes.put(JSONObject().put("key", key).put("value", value))
}
val fromGlobe = point.getJSONObject("cart_attributes")
for (key in fromGlobe.keys()) {
val value = fromGlobe.getString(key)
if (value.isNotEmpty()) attributes.put(JSONObject().put("key", key).put("value", value))
}
val result = storefront(
"mutation(\$cartId: ID!, \$attributes: [AttributeInput!]!) { cartAttributesUpdate(cartId: \$cartId, attributes: \$attributes) { userErrors { message } } }",
JSONObject().put("cartId", cartId).put("attributes", attributes),
)
val errors = result.getJSONObject("cartAttributesUpdate").getJSONArray("userErrors")
if (errors.length() > 0) throw IOException(errors.getJSONObject(0).getString("message"))
}

Use FusedLocationProviderClient for the buyer’s location, or Geocoder to turn a typed address into coordinates.

const GLOBE_API = 'https://api-globe.appfleece.app/v1';
const GLOBE_PUBLISHABLE_KEY = 'glb_pk_…';
const STOREFRONT_URL = 'https://your-store.myshopify.com/api/2026-10/graphql.json';
const STOREFRONT_TOKEN = '…';
const PICKUP_KEYS = [
'globe_PickupPointId', 'globe_PickupPointName', 'globe_PickupPointAddress', 'globe_PickupPointData',
'globe_DeliveryMethod', 'globe_PickupPointCustomerId', 'globe_PickupPointProviderShopId', 'globe_PickupPointCarrier',
];
type Attribute = { key: string; value: string | null };
export type StorefrontPickupPoint = {
id: string;
carrier: string | null;
carrier_name: string | null;
name: string;
formatted_address: string | null;
latitude: number | null;
longitude: number | null;
distance_km: number | null;
carrier_id: string;
cart_attributes: Record<string, string>;
};
export async function searchPickupPoints(country: string, latitude: number, longitude: number) {
const query = `country=${encodeURIComponent(country)}&latitude=${latitude}&longitude=${longitude}`;
const res = await fetch(`${GLOBE_API}/storefront/pickup-points?${query}`, {
headers: { 'X-Globe-Publishable-Key': GLOBE_PUBLISHABLE_KEY },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error?.code}: ${body.error?.message}`);
return body.data as StorefrontPickupPoint[];
}
async function storefront<T>(query: string, variables: Record<string, unknown>): Promise<T> {
const res = await fetch(STOREFRONT_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Storefront-Access-Token': STOREFRONT_TOKEN },
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (body.errors?.length) throw new Error(body.errors[0].message);
return body.data as T;
}
/** The cart's attributes without Globe's pickup keys, plus `updates` (empty values skipped). */
export function mergeAttributes(existing: Attribute[], updates: Record<string, string>) {
const merged = existing
.filter((attribute) => !PICKUP_KEYS.includes(attribute.key))
.map((attribute) => ({ key: attribute.key, value: attribute.value ?? '' }));
for (const [key, value] of Object.entries(updates)) {
if (value) merged.push({ key, value });
}
return merged;
}
async function writeAttributes(cartId: string, updates: Record<string, string>) {
const { cart } = await storefront<{ cart: { attributes: Attribute[] } | null }>(
'query($cartId: ID!) { cart(id: $cartId) { attributes { key value } } }',
{ cartId },
);
if (!cart) throw new Error('The cart no longer exists');
const { cartAttributesUpdate } = await storefront<{
cartAttributesUpdate: { cart: { checkoutUrl: string }; userErrors: { message: string }[] };
}>(
'mutation($cartId: ID!, $attributes: [AttributeInput!]!) { cartAttributesUpdate(cartId: $cartId, attributes: $attributes) { cart { checkoutUrl } userErrors { message } } }',
{ cartId, attributes: mergeAttributes(cart.attributes, updates) },
);
if (cartAttributesUpdate.userErrors.length) throw new Error(cartAttributesUpdate.userErrors[0].message);
return cartAttributesUpdate.cart;
}
export const savePickupPoint = (cartId: string, point: StorefrontPickupPoint) =>
writeAttributes(cartId, point.cart_attributes);
export const chooseHomeDelivery = (cartId: string) =>
writeAttributes(cartId, { globe_DeliveryMethod: 'home' });

Search from the browser, so each buyer uses their own rate limit bucket. Write the cart in an action with Hydrogen’s cart handler. cart.updateAttributes runs cartAttributesUpdate, so it also replaces the whole list.

Add your storefront’s origin under Allowed web origins if you want only your site to use the key.

app/components/PickupPointPicker.tsx
import { useState } from 'react';
import { useFetcher } from 'react-router';
import type { StorefrontPickupPoint } from '~/lib/globe';
const GLOBE_PUBLISHABLE_KEY = 'glb_pk_…';
export function PickupPointPicker({ country }: { country: string }) {
const fetcher = useFetcher();
const [points, setPoints] = useState<StorefrontPickupPoint[]>([]);
async function search(latitude: number, longitude: number) {
const params = new URLSearchParams({ country, latitude: String(latitude), longitude: String(longitude) });
const res = await fetch(`https://api-globe.appfleece.app/v1/storefront/pickup-points?${params}`, {
headers: { 'X-Globe-Publishable-Key': GLOBE_PUBLISHABLE_KEY },
});
if (res.ok) setPoints((await res.json()).data);
}
return (
<div>
<button
type="button"
onClick={() =>
navigator.geolocation.getCurrentPosition((position) =>
search(position.coords.latitude, position.coords.longitude),
)
}
>
Use my location
</button>
<ul>
{points.map((point) => (
<li key={`${point.carrier_id}:${point.id}`}>
{point.name}, {point.formatted_address}
<fetcher.Form method="post" action="/api/pickup-point">
<input type="hidden" name="cartAttributes" value={JSON.stringify(point.cart_attributes)} />
<button type="submit">Choose</button>
</fetcher.Form>
</li>
))}
</ul>
</div>
);
}
app/routes/api.pickup-point.tsx
import { data, type ActionFunctionArgs } from 'react-router';
const PICKUP_KEYS = [
'globe_PickupPointId', 'globe_PickupPointName', 'globe_PickupPointAddress', 'globe_PickupPointData',
'globe_DeliveryMethod', 'globe_PickupPointCustomerId', 'globe_PickupPointProviderShopId', 'globe_PickupPointCarrier',
];
export async function action({ request, context }: ActionFunctionArgs) {
const form = await request.formData();
const fromGlobe = JSON.parse(String(form.get('cartAttributes') ?? '{}')) as Record<string, string>;
const cart = await context.cart.get();
if (!cart) return data({ error: 'No cart' }, { status: 400 });
const keep = cart.attributes
.filter((attribute) => !PICKUP_KEYS.includes(attribute.key))
.map((attribute) => ({ key: attribute.key, value: attribute.value ?? '' }));
const pickup = Object.entries(fromGlobe)
.filter(([key, value]) => PICKUP_KEYS.includes(key) && value)
.map(([key, value]) => ({ key, value: String(value) }));
const result = await context.cart.updateAttributes([...keep, ...pickup]);
return data({ cart: result.cart, errors: result.errors });
}

The action accepts only Globe’s keys from the form. Globe verifies the point again when the order is created, so a tampered value cannot put a point on the order that the carrier does not have.

The cart fragment in the Hydrogen template already queries attributes { key value }. If you changed it, add the field back.


Errors use the Globe API error body.

StatusCodeCause
400validation_errorA key in the query string
401unauthorizedNo key
401invalid_tokenThe key is unknown or revoked, or it is a secret glb_live_ key
403access_requiredGlobe is not installed on the store
403api_pausedGlobe support paused API access for the store
403plan_requiredThe store has no active Globe plan
403origin_not_allowedThe website is not on the store’s allowed web origins
404not_foundUnknown path
405method_not_allowedOnly GET and OPTIONS are supported
422validation_errorA missing or invalid parameter. details.parameter names it. For an unknown carrier, details.unknown lists the values
429rate_limitedA rate limit is exhausted. See Rate limits
500internal_errorAn error on Globe’s side. Retry later

Both endpoints are in https://api-globe.appfleece.app/v1/openapi.json, under the publishableKey security scheme.