shopify-client
Type-safe, async Rust client for the Shopify Admin and Storefront APIs
Installation • Admin API • Storefront API • Bulk Operations • Docs
// Admin API — orders, subscriptions, bulk exports, webhooks, …
let admin = new;
let order = admin.order.get_with_id.await?;
// Storefront API — products, cart, customer, search, … (behind the `storefront` feature)
let store = new;
let product = store.product.get_by_handle.await?;
Highlights
| Two clients, one crate | ShopifyClient for the Admin API, ShopifyStorefront for the Storefront API — each with its own token, endpoint, and surface. Storefront lives behind a Cargo feature so admin-only apps pay nothing for it. |
| 9 admin service modules | Orders, Subscriptions, Discounts, Cart Transforms, App Installation, Shopify Functions, Shop, Storefront Tokens, Bulk Operations |
| 8 storefront service modules | Products, Collections, Cart, Customer, Search, Content (pages/blogs/articles/menus), Shop, Metaobjects |
| Bulk operations | Prebuilt export templates for products, orders, collections, customers, inventory, and draft orders with typed JSONL parsing |
| Typed everything | Strongly typed requests, responses, filters, JSONL export lines, GraphQL inputs — no raw strings |
| Async / await | Built on reqwest with tokio — non-blocking by default, one shared connection pool |
| Webhook parsing | HMAC-verified parsing for customer data, customer redact, and shop redact compliance webhooks |
| Request callbacks | Optional before/after hooks for logging, metrics, and observability |
Installation
shopify-client is a single crate. The Admin API is always available; the Storefront API is gated behind the storefront Cargo feature.
[]
# Admin API only (the common case)
= "0.19"
# Admin API + Storefront API
= { = "0.19", = ["storefront"] }
With the storefront feature enabled, the Storefront client is available as shopify_client::storefront:
use ShopifyClient; // Admin
use ShopifyStorefront; // Storefront (feature-gated)
The two clients are fully independent — different access tokens, different endpoints, different rate limits. Construct whichever you need; apps that need both construct one of each.
Admin API
The Admin API is what apps use to manage merchant data: orders, products, subscriptions, discounts, app metafields, bulk exports, and more. Requests use an Admin access token sent as the X-Shopify-Access-Token header, against the /admin/api/{version}/graphql.json (and per-resource REST) endpoints.
Getting Started
[]
= "0.19"
use ShopifyClient;
async
API Coverage
| Service | Protocol | Operations |
|---|---|---|
client.order |
REST | get_with_id, get_with_name, patch |
client.subscription |
GraphQL | create_recurring, create_usage, create_combined, cancel, extend_trial, update_capped_amount, create_usage_record, get_active_subscriptions |
client.discount |
GraphQL | create_automatic_app_discount, update_automatic_app_discount, get_discount_nodes |
client.app_installation |
GraphQL | get_current, set_metafields, get_metafield, list_metafields |
client.cart_transform |
GraphQL | create, set_metafields |
client.shopify_functions |
GraphQL | list |
client.shop |
GraphQL | get, get_status |
client.storefront_access_token |
GraphQL | list, create, delete |
client.bulk_operation |
GraphQL | run_query, run_mutation, cancel, get, list, create_staged_upload, export_*, stream_* |
Admin services live under shopify_client::admin::* (also re-exported as shopify_client::services::* for back-compat with pre-0.19 releases). Request/response types live under shopify_client::types::*.
Examples
Orders
// Get by ID
let resp = client.order.get_with_id.await?;
println!;
// Get by name
let resp = client.order.get_with_name.await?;
// Update tags
let patch = PatchOrderRequest ;
client.order.patch.await?;
Subscriptions
use *;
let request = CreateRecurringSubscriptionRequest ;
let resp = client.subscription.create_recurring.await?;
println!;
Shop Info
let resp = client.shop.get.await?;
println!;
println!;
// Lightweight status check — useful for health checks / setup wizards
let status = client.shop.get_status.await?;
println!;
Create automatic discount
use DiscountAutomaticAppInput;
let input = DiscountAutomaticAppInput ;
client.discount.create_automatic_app_discount.await?;
Create cart transform
use ;
let input = new
.with_function_handle
.with_block_on_failure
.with_metafields;
client.cart_transform.create.await?;
Update metafields with CAS
use MetafieldsSetInput;
let metafields = vec!;
client.cart_transform.set_metafields.await?;
Manage storefront access tokens
The Admin API issues the Storefront access tokens used by the Storefront client below.
// Create a public access token for a custom storefront / SDK
let resp = client.storefront_access_token
.create
.await?;
println!;
// List all existing tokens
let tokens = client.storefront_access_token.list.await?;
// Revoke when no longer needed
client.storefront_access_token.delete.await?;
Bulk Operations
The bulk operations API lets you export or import millions of objects asynchronously. This client provides two layers:
| Layer | For | Example |
|---|---|---|
| Prebuilt templates | Common exports with zero GraphQL | client.bulk_operation.export_products(params) |
| Raw operations | Custom queries / mutations | client.bulk_operation.run_query(graphql, None) |
Export Templates
Six prebuilt exports, each with typed filter params and comprehensive field coverage:
| Template | Resource | Children | Filter Struct |
|---|---|---|---|
export_products |
Products | Variants, Media | ProductQueryParams |
export_orders |
Orders | LineItems | OrderQueryParams |
export_collections |
Collections | Products | CollectionQueryParams |
export_customers |
Customers | Addresses | CustomerQueryParams |
export_inventory_items |
InventoryItems | Levels | InventoryItemQueryParams |
export_draft_orders |
DraftOrders | LineItems | DraftOrderQueryParams |
Typed Filters
Build search filters with type safety — no raw query strings:
use *;
use DateFilter;
let params = OrderQueryParams ;
client.bulk_operation.export_orders.await?;
Available filter primitives:
| Filter | Variants |
|---|---|
DateFilter |
Exact, Before, After, OnOrBefore, OnOrAfter, Range |
NumericFilter<T> |
Exact, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual |
Full Export Workflow
Bulk exports are a three-step process: start the export, poll until Shopify finishes, then consume the result. The JSONL result URL points to a temporary file hosted by Shopify that can be multi-GB for large shops (millions of products/orders). The stream_* methods download and parse this file in chunks so your app uses constant memory regardless of file size.
use ShopifyClient;
use ;
use ;
let client = new;
// 1. Start the export
let params = ProductQueryParams ;
let resp = client.bulk_operation.export_products.await?;
let op = resp.bulk_operation_run_query.bulk_operation.ok_or?;
// 2. Poll until Shopify finishes processing
let url = loop ;
// 3. Stream results — constant memory, processes batch_size items at a time
client.bulk_operation.stream_products.await?;
Each prebuilt export has a matching stream_* method that takes a batch_size and an async callback:
| Export | Stream | Line Enum |
|---|---|---|
export_products |
stream_products |
ProductExportLine |
export_orders |
stream_orders |
OrderExportLine |
export_collections |
stream_collections |
CollectionExportLine |
export_customers |
stream_customers |
CustomerExportLine |
export_inventory_items |
stream_inventory_items |
InventoryItemExportLine |
export_draft_orders |
stream_draft_orders |
DraftOrderExportLine |
All stream_* methods accumulate up to batch_size parsed items before invoking your async callback. The last batch may contain fewer items. Lines that fail to parse are silently skipped.
// Process 100 products at a time — great for DB writes, API calls, S3 uploads
client.bulk_operation.stream_products.await?;
Custom Bulk Queries
For queries the templates don't cover, use run_query with your own GraphQL and stream_jsonl to consume the result:
let query = r#"{
products {
edges {
node { id title tags }
}
}
}"#;
client.bulk_operation.run_query.await?;
// ... poll until complete, get the url ...
// Stream raw JSON lines in batches of 100
client.bulk_operation.stream_jsonl.await?;
If you already have JSONL content in memory (e.g. read from a local file), each export enum has a parse_line() method:
use ProductExportLine;
let parsed = parse_line?;
Webhooks
Parse Shopify compliance webhooks with type safety:
use ;
match parse_webhook_with_header
HMAC verification helpers live in shopify_client::webhooks::verify:
use ;
if !verify_hmac_from_headers
Storefront API
Requires the
storefrontCargo feature:shopify-client = { version = "0.19", features = ["storefront"] }
The Storefront API is what custom storefronts, BFFs, edge functions, and headless commerce backends use to read the public catalog and manage shopper carts. Requests use a Storefront access token (different from the Admin token) sent as X-Shopify-Storefront-Access-Token, against the /api/{version}/graphql.json endpoint.
Where to get a Storefront access token: create one in the Shopify Admin UI under Apps → Headless / Custom Storefront, or programmatically via
client.storefront_access_token.createon the Admin API.
Getting Started
[]
= { = "0.19", = ["storefront"] }
use ShopifyStorefront;
async
API Coverage
| Service | Field on ShopifyStorefront |
Operations |
|---|---|---|
store.product |
Products | get_by_id, get_by_handle, get_many, get_recommendations |
store.collection |
Collections | get_by_id, get_by_handle, get_with_products, get_many |
store.cart |
Cart | get, create, add_lines, update_lines, remove_lines, update_note, update_attributes, update_buyer_identity, update_discount_codes, add_gift_card_codes (plus *_without_customer variants) |
store.customer |
Customers | get, login, renew_token, logout, create, update, recover, reset, reset_by_url, activate, activate_by_url, create_address, update_address, delete_address, set_default_address |
store.search |
Search | search, predictive |
store.content |
Pages / Blogs / Articles / Menus | nested: content.pages.{get_by_id, get_by_handle, get_many}, content.blogs.{get_by_id, get_by_handle, get_many, get_with_articles}, content.articles.{get_by_id, get_many}, content.menus.get_by_handle |
store.shop |
Shop info | get, get_localization |
store.metaobject |
Metaobjects | get_by_id, get_by_handle, get_many |
All Storefront types — products, carts, customers, response wrappers, input types, sort key enums, etc. — are generated from YAML specs in types/storefront/ (see Code Generation) and live under shopify_client::storefront::generated::types::*.
Examples
Products
use ShopifyStorefront;
use ;
let store = new;
// Single product by handle (most common)
let resp = store.product.get_by_handle.await?;
if let Some = resp.product
// Single product by GID
let resp = store.product.get_by_id.await?;
// Paginated list with filters
let resp = store.product.get_many.await?;
if let Some = resp.products
// Product recommendations (related, complementary)
let resp = store.product.get_recommendations.await?;
Collections
use CollectionRef;
use ;
// Single collection
let resp = store.collection.get_by_handle.await?;
let resp = store.collection.get_by_id.await?;
// Collection with paginated products inside it (one round-trip)
let resp = store.collection.get_with_products.await?;
// Paginated list of all collections
let resp = store.collection.get_many.await?;
Cart
The cart API has 10 mutations plus a get query, each with a *_without_customer variant for unauthenticated flows that omit buyerIdentity.customer from the response.
use ;
use AttributeInput;
// Create an empty cart
let resp = store.cart.create.await?;
let cart_id = resp.cart_create
.and_then
.map
.ok_or?;
// Add lines
let resp = store.cart.add_lines.await?;
// Update line quantities
store.cart.update_lines.await?;
// Apply a discount code
store.cart.update_discount_codes.await?;
// Attach a logged-in customer
store.cart.update_buyer_identity.await?;
// Cart attributes (free-form key/value)
store.cart.update_attributes.await?;
// Read the latest state — `checkout_url` is what you redirect the shopper to
let resp = store.cart.get.await?;
if let Some = resp.cart
Unauthenticated flows. Every cart method has a *_without_customer variant that uses a Cart fragment which omits buyerIdentity.customer. Use these when you don't have (and don't want to leak the existence of) a customer access token:
// Same shape as above, but the response Cart never includes buyerIdentity.customer
let resp = store.cart.create_without_customer.await?;
store.cart.add_lines_without_customer.await?;
store.cart.update_discount_codes_without_customer.await?;
Customer
The customer API covers the full account lifecycle — signup, login, password reset, address management, order history.
use ;
// Sign up
let resp = store.customer.create.await?;
// Log in — returns an access token + expiry
let resp = store.customer.login.await?;
let token = resp.customer_access_token_create
.and_then
.map
.ok_or?;
// Renew before expiry
let resp = store.customer.renew_token.await?;
// Fetch the customer + first N addresses + first N orders
let resp = store.customer.get.await?;
if let Some = resp.customer
// Update profile
store.customer.update.await?;
// Password recovery → email flow
store.customer.recover.await?;
// After the user clicks the reset link, complete with reset_by_url
let resp = store.customer.reset_by_url.await?;
// Or reset with an explicit id + reset token (e.g. from a deeplink param)
let resp = store.customer.reset.await?;
// Account activation (for accounts created in admin without a password)
let resp = store.customer.activate.await?;
// Addresses
store.customer.create_address.await?;
store.customer.update_address.await?;
store.customer.delete_address.await?;
store.customer.set_default_address.await?;
// Log out (revoke the token)
store.customer.logout.await?;
Search
use ;
// Full search (returns Product | Page | Article in a connection)
let resp = store.search.search.await?;
// Predictive search — for instant-results UIs / typeahead
let resp = store.search.predictive.await?;
if let Some = resp.predictive_search
Content
The content service is split into four sub-services matching Shopify's content model: pages, blogs, articles, and menus.
use BlogRef;
use ;
// Pages
let resp = store.content.pages.get_by_handle.await?;
let resp = store.content.pages.get_by_id.await?;
let resp = store.content.pages.get_many.await?;
// Blogs
let resp = store.content.blogs.get_by_handle.await?;
let resp = store.content.blogs.get_many.await?;
// Blog + its articles in one round-trip
let resp = store.content.blogs.get_with_articles.await?;
// Articles (top-level, across all blogs)
let resp = store.content.articles.get_by_id.await?;
let resp = store.content.articles.get_many.await?;
// Navigation menus
let resp = store.content.menus.get_by_handle.await?;
if let Some = resp.menu
Shop & Localization
let resp = store.shop.get.await?;
if let Some = resp.shop
// Available languages, currencies, and country shopping experiences
let resp = store.shop.get_localization.await?;
if let Some = resp.localization
Metaobjects
use ;
// By GID
let resp = store.metaobject.get_by_id.await?;
// By {type, handle} tuple
let resp = store.metaobject.get_by_handle.await?;
// All metaobjects of a given type, paginated
let resp = store.metaobject.get_many.await?;
Storefront vs Admin: when to use which
| Need | API | Service |
|---|---|---|
| Display the catalog on a custom storefront | Storefront | store.product, store.collection |
| Manage a shopper's cart and redirect to checkout | Storefront | store.cart |
| Customer signup / login / address book | Storefront | store.customer |
| Process an order after it's placed | Admin | client.order |
| Run a one-off export of all products | Admin | client.bulk_operation.export_products |
| Charge a subscription | Admin | client.subscription |
| Define a discount that affects all storefronts | Admin | client.discount |
| Issue a Storefront access token for a new headless app | Admin | client.storefront_access_token.create |
Request Callbacks
Both clients support optional before/after hooks on every request — useful for logging, metrics, and tracing.
use ShopifyClient;
use Arc;
let client = new_with_callbacks;
ShopifyStorefront::new_with_callbacks has the exact same signature. Properties:
- Fires on every REST and GraphQL request.
- The shop-level access token is never passed to callbacks.
- The request
bodycan contain other sensitive values that travel as GraphQL variables — most notably a customer access token on storefront cart/customer mutations, and passwords oncustomerCreate/customerReset. Treat the body as sensitive; avoid logging it verbatim to shared destinations. - Panic-safe — a panicking callback won't crash the request flow.
Error Handling
All methods on both clients return Result<T, APIError>:
For GraphQL errors, APIError::ServerError.errors includes Shopify's error code where available — e.g. [THROTTLED] Throttled — so callers can classify failures for retry without parsing free text.
For Storefront mutations that return per-field validation errors (e.g. customerCreate with a bad email), check the customer_user_errors / user_errors field on the response payload — those are part of a successful response, not an APIError.
Code Generation
Storefront types are generated from YAML specs in types/storefront/ via type-crafter, invoked through npx:
The generated output is checked into git, so cargo build works without Node installed. Regenerate after editing any types/storefront/*.yaml file.
Project Structure
shopify-rust-client/
├── Cargo.toml # single [package]; `storefront` feature
├── Makefile # `make gen-storefront` regenerates storefront types
├── types/storefront/*.yaml # source specs for the generated storefront types
└── src/
├── lib.rs # ShopifyClient
├── common/ # shared infra
│ ├── types.rs # APIError, RequestCallbacks, PageInfo, …
│ ├── http.rs # shared reqwest client + GraphQL executors
│ ├── utils.rs # parse_response helpers
│ ├── query_filter.rs # DateFilter, NumericFilter
│ └── mod.rs # ServiceContext
├── admin/ # admin services (also re-exported as `services`)
│ └── <service>/{mod.rs, remote.rs} # order, subscription, discount, …, bulk_operation
├── types/ # admin request/response types
├── webhooks/ # compliance webhook parsing + HMAC verify
└── storefront/ # behind the `storefront` feature
├── mod.rs # ShopifyStorefront
├── fragments/ # GraphQL fragments shared across services
├── <service>/ # product, cart, collection, customer, …
│ ├── mod.rs # service struct + async methods
│ ├── remote.rs # internal HTTP calls
│ └── queries.rs # GraphQL query builders
└── generated/types/ # type-crafter output (checked in)
Requirements
- Rust 2021 edition (1.70+)
- Always:
reqwest,serde,serde_json,hmac,sha2,base64 - Only with the
storefrontfeature:time(typedOffsetDateTimetimestamps on generated Storefront types)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. If you change any types/storefront/*.yaml spec, run make gen-storefront and commit the regenerated output.
License
This project is licensed under the MIT License.
This is an unofficial client library. For official Shopify documentation, visit shopify.dev.