22 toolsAPI key
shopify_example.py
Authenticating
This pack takes an API key inX-Shopify-Access-Token.
There is no authorization server, no consent screen and no refresh — API keys is the whole story.
Two values, because a Shopify installation is a store as well as a token:
configure() is optional when both $SHOPIFY_SHOP and
$SHOPIFY_ACCESS_TOKEN are set. shop is the subdomain — "my-store" for
my-store.myshopify.com — and the full domain or a complete URL is accepted
too, since all three appear in Shopify’s own documentation and admin UI.
The client
api_key_tool_factory is the whole client: a thin wrapper over httpx that attaches your key and these endpoint constants to each request. ShopifyAPI does not enter your dependency tree.Paging through a list
A cursor belongs to the tool that returns it, so it is declared on that tool’s builder call:shopify_pagination.py
Pagination is declared on
products_list, orders_list, customers_list and locations_list. The other 18 take no cursor.Tools
Each is aTool, called with
ainvoke as in the snippet above. The name
links to its parameters, its response and what it costs.
shop_getPOSTGet the store’s own details — its name, domain, currency and timezone.products_listPOSTList products.product_getPOSTGet one product with its description and its first 50 variants, including each variant’s SKU, price and inventory.product_createPOSTCreate a product.product_updatePOSTUpdate a product.orders_listPOSTList orders.order_getPOSTGet one order with its line items, shipping address and totals.customers_listPOSTList customers.customer_getPOSTGet one customer, with lifetime spend and default address.customer_createPOSTCreate a customer.customer_updatePOSTUpdate a customer.order_fulfillment_ordersPOSTList an order’s fulfillment orders, which is the first half of fulfilling it.fulfillment_createPOSTFulfil one or more fulfillment orders, marking the goods as shipped.inventory_adjust_quantitiesPOSTMove stock by a relative amount:
The API version sits in the path, and pinning it is not optional: Shopify ships a
new version quarterly and supports each for twelve months, so an unpinned
integration is one that changes under you. delta of 5 adds five, -5 removes five.variant_inventory_levelPOSTRead a variant’s stock at one location.locations_listPOSTList the store’s locations.order_closePOSTClose an order, marking it finished.order_openPOSTReopen a closed order.order_cancelPOSTCancel an order.draft_order_createPOSTCreate a draft order, the way to build an order by hand before it is placed.draft_order_completePOSTTurn a draft order into a real one.product_variants_bulk_createPOSTAdd variants to a product.shopify.API_VERSION is the version
these documents were written against.
A host that is not a constant
Every other pack here has a fixed base URL. A Shopify store lives athttps://{shop}.myshopify.com/, so the host is only known once someone installs
the app. base_url therefore takes a callable, resolved on every request:
shopify_resolve_base_url.py
Path() parameter for the subdomain
would let the model choose which server the request goes to, which is the one
thing a boundary cannot permit. An unresolved base URL raises CredentialError
before anything leaves the process, the same way an unconfigured credential does.
Zendesk, Atlassian and self-hosted GitLab have the same shape. See
the wire contract.
Failure hides in userErrors
As with Linear, GraphQL’serrors array only carries problems with the
document. A mutation Shopify ran and then refused — a duplicate handle, a
customer with no contact details — returns HTTP 200, no errors, and a populated
userErrors:
shopify_user_errors.json
errors_field takes several paths, so one declaration covers both places:
shopify_envelope.py
userErrors, and a successful mutation has an empty one, so
neither matches. The error message names the resolved path, so it says which
write was declined without the envelope knowing anything about Shopify.
Money comes back as a MoneyBag
Shopify returns every price as{"shopMoney": {"amount": "10.00", "currencyCode": "USD"}}, because a store can
present one currency and settle in another. Two levels of nesting per price, on
every line item of every order, is a lot of envelope for a number.
Every tool here runs an unwrap handler that strips GraphQL’s data and operation
name and flattens the shop-currency side to "10.00 USD". The walk covers the
whole payload, since prices appear on the order, on each line item, and on a
customer’s lifetime spend.
Gotchas
Ids are global, and they are not numbers
Ids are global, and they are not numbers
product_get, order_get and customer_get take a global id —
gid://shopify/Product/123456 — not the bare number from the admin URL. The
list tools return ids in that form, which is the intended way to get one.Filtering is an untyped search string
Filtering is an untyped search string
Shopify’s
query argument takes its own syntax rather than a filter object,
so it cannot be typed the way Linear’s IssueFilter can. The supported
qualifiers are documented on each field instead:
financial_status:paid fulfillment_status:unshipped for orders waiting to
ship, status:active vendor:Acme for products, orders_count:>5 for
customers.Orders stop 60 days back unless you were granted a scope
Orders stop 60 days back unless you were granted a scope
Shopify serves an app holding
read_orders only the last 60 days of a
store’s orders. Everything older needs read_all_orders, which Shopify
grants by review rather than on request.The truncation is silent — no error, no marker on the page, just a shorter
history than the store has — so orders_list says so in the description the
model reads, and a created_at: window reaching further back is called out
on the query field. An empty result is not evidence of an empty history.Rate limiting is a cost budget, not a request count
Rate limiting is a cost budget, not a request count
Shopify prices each query by the fields it selects, so no constant
quota_cost would be truthful — the cost of a call depends on the page size
it is invoked with. The selection sets in charter.packs.shopify.queries are
deliberately narrow for the same reason, and keeping first small is worth
doing.A new product is not on the storefront
A new product is not on the storefront
Shopify creates products unpublished.
product_create returns successfully
and the product is invisible to shoppers until it is published to a sales
channel, which is a separate operation and is not modelled here.A customer needs an email or a phone number
A customer needs an email or a phone number
customer_create with neither is refused — and refused through
userErrors, at HTTP 200, which is exactly the case the envelope exists for.Related
- Linear — the other GraphQL pack
- Envelopes — several error paths in one declaration
- The wire contract — a base URL that is not a constant