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.
322 lines
15 KiB
Markdown
322 lines
15 KiB
Markdown
# 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.
|