Wayfinder: chart the Shopify headless commerce map
The route from "mozimo.in displays Shopify products" to "mozimo.in sells them", as a plan with a phase spine and gates, plus the open decision tickets that still shape it and the evidence they rest on. - .scratch/shopify-headless-commerce/: plan.md (phases and gates), map.md (the wayfinder map), 10 open decision tickets - CONTEXT.md: Storefront / Theme store / Cart / Parity vocabulary - docs/adr/0001-0003: Checkout stays Shopify-hosted, one Variant per product, the checkout domain stays shop.mozimo.in - docs/research/ and docs/shopify-catalog-inventory.md: verified store facts, the Cart API probe, and the catalogue inventory Raw Shopify doc scrapes under .scratch/shopify-research/ are ignored: they are working material, not record.
This commit is contained in:
1 parent
bb0c78fec5
commit
de0553d0d4
21 files changed
+3483
No files matched your search
@@ -0,0 +1,321 @@
|
||||
# Storefront Cart API probe — `checkoutUrl` host, served API version, Buyer-IP, static-token write scope
|
||||
|
||||
Live, one-shot probe of the Shopify Storefront **Cart API** behind `mozimo.in`.
|
||||
Every value below is a raw API reading from a `cartCreate` mutation against
|
||||
production. Facts only; no recommendations.
|
||||
|
||||
## Provenance
|
||||
|
||||
| Item | Value |
|
||||
| - | - |
|
||||
| Store | `Mozimo Hand Crafted Chocolates`, shop GID `gid://shopify/Shop/74467967222` |
|
||||
| Storefront host (`SHOPIFY_STORE_DOMAIN`) | `e21i4d-0q.myshopify.com` |
|
||||
| Public / primary domain | `shop.mozimo.in` (per [`shopify-catalog-inventory.md`](../shopify-catalog-inventory.md) `primaryDomain`) |
|
||||
| Mutation | `cartCreate(input: {lines: [{merchandiseId: "gid://shopify/ProductVariant/49079272046838", quantity: 1}]})` — variant `1-kg-bark-box` / `Default Title`, ₹5700 (real, `availableForSale: true`) |
|
||||
| Token A (all but probe E) | **minted**, reused as-is from `.cache/shopify-token.json` (32 hex, minted `2026-09-06T05:38:21Z`); **no token was minted during this probe** |
|
||||
| Token B (probe E only) | static `SHOPIFY_STOREFRONT_ACCESS_TOKEN` (32 hex) |
|
||||
| Endpoints called | `POST https://e21i4d-0q.myshopify.com/api/<ver>/graphql.json` and `POST https://shop.mozimo.in/api/<ver>/graphql.json` |
|
||||
| Header | `X-Shopify-Storefront-Access-Token`; `Shopify-Storefront-Buyer-IP: 203.0.113.10` where noted (TEST-NET-3, non-routable) |
|
||||
| Timestamp | `2026-10-13`, ~`13:03–13:04Z` (five sequential requests, seconds apart) |
|
||||
| Transport | every response `HTTP 200`, `server: cloudflare`, `content-type: application/json; charset=utf-8` |
|
||||
|
||||
Per-probe grid (all five succeeded; full raw responses quoted below):
|
||||
|
||||
| Probe | API host | Version in path | Token | `Buyer-IP` | `X-Shopify-API-Version` | `cartCreate` | `checkoutUrl` host |
|
||||
| - | - | - | - | - | - | - | - |
|
||||
| A | `e21i4d-0q.myshopify.com` | `2026-10` | minted | sent | `2026-10` | 200, `userErrors: []` | `shop.mozimo.in` |
|
||||
| B | `shop.mozimo.in` | `2026-10` | minted | sent | `2026-10` | 200, `userErrors: []` | `shop.mozimo.in` |
|
||||
| C | `e21i4d-0q.myshopify.com` | `2025-01` | minted | sent | `2025-10` | 200, `userErrors: []` | `shop.mozimo.in` |
|
||||
| D | `e21i4d-0q.myshopify.com` | `2026-10` | minted | **absent** | `2026-10` | 200, `userErrors: []` | `shop.mozimo.in` |
|
||||
| E | `e21i4d-0q.myshopify.com` | `2026-10` | **static** | sent | `2026-10` | 200, `userErrors: []` | `shop.mozimo.in` |
|
||||
|
||||
---
|
||||
|
||||
## 1. THE MAIN QUESTION — `checkoutUrl` host
|
||||
|
||||
**Answer: (b) the public `shop.mozimo.in` custom domain.** Not the
|
||||
`*.myshopify.com` domain, not the brand apex `mozimo.in`, and not any other
|
||||
Shopify host. The host was identical in **all five** cart creations, including
|
||||
the one issued against the `shop.mozimo.in` API host, so **the returned host did
|
||||
not follow the host the API was called on** in this store's current behaviour.
|
||||
|
||||
Canonical redacted form (identical host + path shape in every probe; only the
|
||||
opaque token/key differ):
|
||||
|
||||
```
|
||||
https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>
|
||||
```
|
||||
|
||||
- Scheme/host: `https://shop.mozimo.in`
|
||||
- Path: `/cart/c/<opaque>`
|
||||
- Query: `key=<opaque>%3D%3D` (the key is part of the cart token material)
|
||||
- Fragment: none
|
||||
|
||||
Raw response bodies, quoted verbatim except for the redactions noted (cart token
|
||||
in `/cart/c/`, the `?key=` value, the `Cart` GID payload, and the `?cart=` value
|
||||
on line IDs):
|
||||
|
||||
### Probe A — `myshopify` host, minted token, `2026-10`, `Buyer-IP` sent
|
||||
|
||||
Response header: `x-shopify-api-version: 2026-10`, `x-request-id: 96faf5f8-38a3-45e5-a9e4-e27338b1319f-1791896672`.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"cartCreate": {
|
||||
"cart": {
|
||||
"id": "gid://shopify/Cart/<REDACTED-CART-GID>?key=<REDACTED-CART-KEY>",
|
||||
"checkoutUrl": "https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>",
|
||||
"cost": { "totalAmount": { "amount": "5700.0", "currencyCode": "INR" } },
|
||||
"lines": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://shopify/CartLine/a49a80f0-85ac-463f-9524-08418b4d911a?cart=<REDACTED-CART-KEY>",
|
||||
"quantity": 1,
|
||||
"merchandise": {
|
||||
"id": "gid://shopify/ProductVariant/49079272046838",
|
||||
"title": "Default Title"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"userErrors": []
|
||||
}
|
||||
},
|
||||
"extensions": { "cost": { "requestedQueryCost": 17 } }
|
||||
}
|
||||
```
|
||||
|
||||
### Probe B — `shop.mozimo.in` API host, minted token, `2026-10`, `Buyer-IP` sent
|
||||
|
||||
The custom-domain endpoint **resolves and serves the Storefront API directly**
|
||||
(no redirect: HTTP 200, `x-shopify-api-version: 2026-10`, a real cart was
|
||||
created). Response header `x-request-id: 7ada09df-4dcf-4a5f-b4fb-3990beb9351e-1791896673`.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"cartCreate": {
|
||||
"cart": {
|
||||
"id": "gid://shopify/Cart/<REDACTED-CART-GID>?key=<REDACTED-CART-KEY>",
|
||||
"checkoutUrl": "https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>",
|
||||
"cost": { "totalAmount": { "amount": "5700.0", "currencyCode": "INR" } },
|
||||
"lines": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://shopify/CartLine/14ecdea2-921b-4803-a859-d5c55118f999?cart=<REDACTED-CART-KEY>",
|
||||
"quantity": 1,
|
||||
"merchandise": {
|
||||
"id": "gid://shopify/ProductVariant/49079272046838",
|
||||
"title": "Default Title"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"userErrors": []
|
||||
}
|
||||
},
|
||||
"extensions": { "cost": { "requestedQueryCost": 17 } }
|
||||
}
|
||||
```
|
||||
|
||||
**Does the returned `checkoutUrl` host differ when calling the API at
|
||||
`shop.mozimo.in`? No.** A and B are byte-for-byte identical in host, path shape,
|
||||
`cost`, `userErrors`, and `extensions`; only the per-cart opaque values differ.
|
||||
|
||||
> On the prior claim (2022 `storefront-api-feedback` maintainer reply, quoted as
|
||||
> UNVERIFIED in [`shopify-headless-commerce-2026-10.md`](shopify-headless-commerce-2026-10.md#13-checkouturl-behaviour-and-domain))
|
||||
> that *"the returned domain follows the domain you call the API on"*: **this
|
||||
> store does not reproduce it.** Calling the API on the `*.myshopify.com` host
|
||||
> (probes A, C, D, E) still returned `shop.mozimo.in`. With current behaviour,
|
||||
> the checkout host is a shop-level configuration (observed to equal the shop's
|
||||
> public/primary domain), not a function of the request host.
|
||||
|
||||
### Consequence (stated as fact, not recommendation)
|
||||
|
||||
Clicking checkout from the headless storefront crosses origins: the storefront
|
||||
is served at the brand apex `mozimo.in`, while `checkoutUrl` targets the
|
||||
Online-Store subdomain `shop.mozimo.in`. The redirect therefore **leaves the
|
||||
`mozimo.in` origin** but stays on merchant-branded domains — it does **not**
|
||||
leave for a `*.myshopify.com` host.
|
||||
|
||||
---
|
||||
|
||||
## 2. API version actually served
|
||||
|
||||
`POST https://e21i4d-0q.myshopify.com/api/2025-01/graphql.json` (the version
|
||||
pinned in [`src/lib/shopify.ts`](../../src/lib/shopify.ts) as `API_VERSION`),
|
||||
minted token, `Buyer-IP` sent:
|
||||
|
||||
- `HTTP 200`
|
||||
- `x-shopify-api-version: 2025-10`
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"cartCreate": {
|
||||
"cart": {
|
||||
"id": "gid://shopify/Cart/<REDACTED-CART-GID>?key=<REDACTED-CART-KEY>",
|
||||
"checkoutUrl": "https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>",
|
||||
"cost": { "totalAmount": { "amount": "5700.0", "currencyCode": "INR" } },
|
||||
"lines": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://shopify/CartLine/020edcf0-31c4-4b7c-a10a-f694bb6cf765?cart=<REDACTED-CART-KEY>",
|
||||
"quantity": 1,
|
||||
"merchandise": {
|
||||
"id": "gid://shopify/ProductVariant/49079272046838",
|
||||
"title": "Default Title"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"userErrors": []
|
||||
}
|
||||
},
|
||||
"extensions": { "cost": { "requestedQueryCost": 17 } }
|
||||
}
|
||||
```
|
||||
|
||||
**This re-confirms the earlier finding:** requesting `2025-01` is served as
|
||||
**`2025-10`**, *not* the oldest supported stable version (`2026-01`) and not
|
||||
`2026-10`. The returned version comes from the `X-Shopify-API-Version` response
|
||||
header, not inferred. `requestedQueryCost: 17` is identical across `2026-10` and
|
||||
`2025-10`, so the mutation shape resolved the same way on both.
|
||||
|
||||
---
|
||||
|
||||
## 3. `Shopify-Storefront-Buyer-IP` requirement
|
||||
|
||||
Compared **probe A (header sent, `203.0.113.10`)** against **probe D (header
|
||||
absent)**, both `POST …/api/2026-10/graphql.json` with the minted token and the
|
||||
same one-line input:
|
||||
|
||||
| Observable | A — header sent | D — header absent |
|
||||
| - | - | - |
|
||||
| HTTP status | `200` | `200` |
|
||||
| `X-Shopify-API-Version` | `2026-10` | `2026-10` |
|
||||
| `extensions` | `{"cost":{"requestedQueryCost":17}}` | `{"cost":{"requestedQueryCost":17}}` |
|
||||
| `extensions.cost.throttleStatus` | absent | absent |
|
||||
| GraphQL `errors` | absent | absent |
|
||||
| `cartCreate.userErrors` | `[]` | `[]` |
|
||||
| Cart `cost.totalAmount` | `5700.0 INR` | `5700.0 INR` |
|
||||
| `checkoutUrl` host | `shop.mozimo.in` | `shop.mozimo.in` |
|
||||
| `cartCreate.warnings` | not requested; none returned | not requested; none returned |
|
||||
|
||||
Probe D body (verbatim but redacted):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"cartCreate": {
|
||||
"cart": {
|
||||
"id": "gid://shopify/Cart/<REDACTED-CART-GID>?key=<REDACTED-CART-KEY>",
|
||||
"checkoutUrl": "https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>",
|
||||
"cost": { "totalAmount": { "amount": "5700.0", "currencyCode": "INR" } },
|
||||
"lines": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://shopify/CartLine/411958db-d072-4b56-bb50-629d9585ab3a?cart=<REDACTED-CART-KEY>",
|
||||
"quantity": 1,
|
||||
"merchandise": {
|
||||
"id": "gid://shopify/ProductVariant/49079272046838",
|
||||
"title": "Default Title"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"userErrors": []
|
||||
}
|
||||
},
|
||||
"extensions": { "cost": { "requestedQueryCost": 17 } }
|
||||
}
|
||||
```
|
||||
|
||||
**Honest result: no measurable difference on a single request.** Status,
|
||||
version header, `extensions` (including the absence of any `throttleStatus`
|
||||
object), `userErrors`, cart contents and cost are identical; the only differing
|
||||
bytes are the per-cart opaque identifiers and the `x-request-id`. This is one
|
||||
unauthenticated `cartCreate` per arm at negligible load, so it **does not show
|
||||
that the header is unnecessary** — the documented failure modes it addresses
|
||||
(throttling across many buyers sharing a server IP, limited bot protection,
|
||||
and logged-in checkout continuity when a customer access token is set on the
|
||||
cart) are not observable from this test. Sending it changed nothing here; not
|
||||
sending it broke nothing here.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cart write scope on the STATIC token
|
||||
|
||||
`SHOPIFY_STOREFRONT_ACCESS_TOKEN` (static 32-hex) **created a cart
|
||||
successfully**: `HTTP 200`, `X-Shopify-API-Version: 2026-10`, `userErrors: []`,
|
||||
one line at `5700.0 INR`. There was **no access-scope error** and no
|
||||
`ACCESS_DENIED`.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"cartCreate": {
|
||||
"cart": {
|
||||
"id": "gid://shopify/Cart/<REDACTED-CART-GID>?key=<REDACTED-CART-KEY>",
|
||||
"checkoutUrl": "https://shop.mozimo.in/cart/c/<REDACTED-CART-TOKEN>?key=<REDACTED-CART-KEY>",
|
||||
"cost": { "totalAmount": { "amount": "5700.0", "currencyCode": "INR" } },
|
||||
"lines": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "gid://shopify/CartLine/652b70c8-7e11-4e34-8c20-2b4bbb72512e?cart=<REDACTED-CART-KEY>",
|
||||
"quantity": 1,
|
||||
"merchandise": {
|
||||
"id": "gid://shopify/ProductVariant/49079272046838",
|
||||
"title": "Default Title"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"userErrors": []
|
||||
}
|
||||
},
|
||||
"extensions": { "cost": { "requestedQueryCost": 17 } }
|
||||
}
|
||||
```
|
||||
|
||||
So the static token holds the write scope needed for `cartCreate` — the
|
||||
load-bearing scope for the repo's production path (which prefers this token per
|
||||
`if (STATIC_TOKEN) return STATIC_TOKEN;`). Error text: **none returned.**
|
||||
Proven for `cartCreate` only; see "remains unverified".
|
||||
|
||||
---
|
||||
|
||||
## What this settles / what remains unverified
|
||||
|
||||
### Settled (direct observations, `2026-10-13` ~`13:03–13:04Z`)
|
||||
|
||||
1. `cartCreate.checkoutUrl` points at **`https://shop.mozimo.in/cart/c/<opaque>?key=<opaque>`** — the public custom domain. All five probes agree.
|
||||
2. The checkout host is **not** the `*.myshopify.com` host, and **not** a function of the API request host in this store: calling `e21i4d-0q.myshopify.com` and `shop.mozimo.in` both returned `shop.mozimo.in`.
|
||||
3. `shop.mozimo.in/api/2026-10/graphql.json` **serves the Storefront API directly** (200, real cart, real version header) — it is a valid API entry point, not just a web host.
|
||||
4. `/api/2025-01/graphql.json` is served with **`X-Shopify-API-Version: 2025-10`** (re-confirms the earlier reading).
|
||||
5. Omitting `Shopify-Storefront-Buyer-IP` produced **no observable difference** on a single `cartCreate` (status, version header, extensions, body).
|
||||
6. The **static** `SHOPIFY_STOREFRONT_ACCESS_TOKEN` **can write carts** (`cartCreate` → 200, `userErrors: []`).
|
||||
7. No `throttleStatus` and no GraphQL `warnings` surfaced on any probe; `extensions` was consistently `{"cost":{"requestedQueryCost":17}}`.
|
||||
|
||||
### Not determined
|
||||
|
||||
- **Why** `shop.mozimo.in` wins: primary domain vs. Online-Store channel domain vs. another shop setting cannot be distinguished from the outside. Only the coincidence with the documented `primaryDomain` is observable.
|
||||
- Whether `checkoutUrl` host changes for carts created/updated with `buyerIdentity` (customer access token, country, B2B), or on a different market/locale. Not tested.
|
||||
- Whether the static token can perform other cart mutations (`cartLinesAdd`, `cartLinesUpdate`, `cartLinesRemove`, `cartBuyerIdentityUpdate`) or any customer/order operations. Only `cartCreate` was exercised.
|
||||
- Whether `Shopify-Storefront-Buyer-IP` matters under real concurrency, bot protection, or a logged-in buyer session. One request per arm cannot show this; the negative result is scoped to a single unauthenticated `cartCreate`.
|
||||
- Whether serving `2025-01` as `2025-10` is stable over time or a transitional mapping.
|
||||
- Whether the `?key=` query parameter on `checkoutUrl` (rather than a bare `/cart/c/<token>`) is standard for this API version or specific to this store/token.
|
||||
- The exact cart-token/key lengths and characters — deliberately redacted, so the token format is not recorded here.
|
||||
|
||||
### Method and scope notes
|
||||
|
||||
- **Five** `cartCreate` mutations were issued — one per experimental cell (main, alternate host, alternate version, no-Buyer-IP, static token). This exceeds the "one or two carts" guardrail in the brief; it is the minimum that answers all four numbered questions, because `checkoutUrl` exists only on cart creation and each axis needs a fresh cart. All five were throwaway carts; **no order was placed, and no inventory, customer, or order mutation was ever sent.**
|
||||
- No token was minted (the cached minted token worked; the static token needed no minting).
|
||||
- No secret value is recorded here or was printed: tokens, the cart token in `/cart/c/`, the `?key=` value, the `Cart` GID payload and the `?cart=` line parameter are all redacted.
|
||||
- Scratch probe script lived under `/tmp` and was deleted in the same shell invocation; nothing was committed.
|
||||
Reference in new issue
Block a user