Storefront pickup API
Ce contenu n’est pas encore disponible dans votre langue.
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.
Authentication
Section titled “Authentication”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 gets401 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
Originheader, 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_allowedwithout CORS headers, so the browser hides the response from the page. - Preflight
OPTIONSrequests answer204and are cached for 10 minutes.
Rate limits
Section titled “Rate limits”| Bucket | Limit |
|---|---|
storefront_ip_per_minute | 60 requests per minute per key and client IP |
storefront_key_per_minute | 600 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
Section titled “Responses”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.
List carriers
Section titled “List carriers”GET /v1/storefront/carriers?country=CZReturns 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.
| Parameter | Required | Description |
|---|---|---|
country | Yes | The 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}| Field | Description |
|---|---|
id | Your Globe carrier instance. One carrier can have several instances, for example two accounts |
code | The carrier code |
name | The carrier’s name as buyers see it in this country |
icon | The carrier’s icon URL, or null |
types | The point types this instance offers: code and name. Pass a code as type to the search |
Search pickup points
Section titled “Search pickup points”GET /v1/storefront/pickup-pointsReturns your pickup points nearest to a place, nearest first. Only your carriers that serve country are searched.
| Parameter | Required | Description |
|---|---|---|
country | Yes | The delivery country, ISO 3166-1 alpha-2 |
latitude, longitude | One of the two places | Search around these coordinates |
postal_code | One of the two places | Search around this postal code when no coordinates are sent. 2 to 16 letters, digits, spaces or hyphens |
city | No | Narrows a postal_code search. At most 80 characters |
carrier | No | Comma-separated carrier instance IDs or carrier codes from List carriers. At most 20 |
type | No | Comma-separated point type codes. At most 20 |
limit | No | 1 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.
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}| Field | Description |
|---|---|
id | The point’s ID at the carrier |
carrier, carrier_name | The carrier code and its name as buyers see it |
name, type | The point’s name and type |
address | street, street_number, city, postal_code, country |
formatted_address | The address on one line |
latitude, longitude | The point’s coordinates |
distance_km | Distance from the searched place |
url | The point’s page at the carrier, or null |
carrier_id | Your carrier instance that serves the point, the id from List carriers |
opening_hours | The carrier’s opening hours as Globe stores them. The shape varies by carrier. null when unknown |
cart_attributes | The 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.
Writing to the cart
Section titled “Writing to the cart”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_PickupPointIdglobe_PickupPointNameglobe_PickupPointAddressglobe_PickupPointDataglobe_DeliveryMethodglobe_PickupPointCustomerIdglobe_PickupPointProviderShopIdglobe_PickupPointCarrierFor 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.
Examples
Section titled “Examples”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.
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 @-Swift (URLSession)
Section titled “Swift (URLSession)”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.
Kotlin (OkHttp)
Section titled “Kotlin (OkHttp)”Run these functions off the main thread, for example in Dispatchers.IO.
import okhttp3.HttpUrl.Companion.toHttpUrlimport okhttp3.MediaType.Companion.toMediaTypeimport okhttp3.OkHttpClientimport okhttp3.Requestimport okhttp3.RequestBody.Companion.toRequestBodyimport org.json.JSONArrayimport org.json.JSONObjectimport 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.
React Native and TypeScript (fetch)
Section titled “React Native and TypeScript (fetch)”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' });Hydrogen
Section titled “Hydrogen”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.
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> );}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
Section titled “Errors”Errors use the Globe API error body.
| Status | Code | Cause |
|---|---|---|
400 | validation_error | A key in the query string |
401 | unauthorized | No key |
401 | invalid_token | The key is unknown or revoked, or it is a secret glb_live_ key |
403 | access_required | Globe is not installed on the store |
403 | api_paused | Globe support paused API access for the store |
403 | plan_required | The store has no active Globe plan |
403 | origin_not_allowed | The website is not on the store’s allowed web origins |
404 | not_found | Unknown path |
405 | method_not_allowed | Only GET and OPTIONS are supported |
422 | validation_error | A missing or invalid parameter. details.parameter names it. For an unknown carrier, details.unknown lists the values |
429 | rate_limited | A rate limit is exhausted. See Rate limits |
500 | internal_error | An error on Globe’s side. Retry later |
OpenAPI
Section titled “OpenAPI”Both endpoints are in https://api-globe.appfleece.app/v1/openapi.json, under the publishableKey security scheme.