Shopify POS is Shopify's in-store point of sale: an iOS and Android app, the POS Go handheld, and Shopify's card readers and terminals. For an integrator the most important fact is that there is no POS API. A retail sale is an ordinary Shopify order in the same store, written into the same Admin API, and the only things that mark it as a store sale are a handful of fields: sourceName, retailLocation, staffMember, app and publication. Stock in a shop is an ordinary InventoryLevel at an ordinary Location. So a connector that already reads Shopify reads POS for free, and the real work is filtering and attribution rather than a second integration. The one genuinely POS-specific developer surface is POS UI extensions, which put your app's tiles, blocks and modals inside the POS app.
At a glance
What it is
Shopify POS turns a Shopify store into an omnichannel retailer. It ships in two tiers, POS Lite (included with every Shopify plan) and POS Pro (a paid per-location subscription that adds smart grid customisation, staff permissions, stocktaking and retail analytics). Exact pricing was not verified for this note and drifts, so check the current Shopify retail pricing page before quoting it.
Geographically POS payments are supported in a limited and growing set of countries, because card present processing needs local acquiring. Everything else, the order model and the API, is identical worldwide. Crucially, POS is a sales channel on the same store, not a separate account. One Shopify store can have an online store, several physical shops, and a set of third party channels, and all of their orders land in the same orders connection. That is convenient for ingest and dangerous for reporting: a naive Shopify connector will silently blend online and retail revenue unless it splits on sourceName.
API access
There is nothing POS-specific to apply for. Two routes to credentials:
- Custom app created by the merchant in their own admin under Settings, Apps and sales channels, Develop apps. The merchant picks Admin API scopes and reveals a long-lived Admin API access token, prefixed
shpat_. This is the right route for a single-merchant data pipeline. - Public or custom app created in the Shopify Partner dashboard and installed through OAuth. This is the route for a multi-merchant connector, and the only route if you also want POS UI extensions, because extensions are deployed as part of an app with the Shopify CLI.
API versions are quarterly and named by date, for example 2026-07. Each stable version is supported for a minimum of 12 months. The version is in the URL for the Admin API and in the TOML file for POS UI extensions, and the Shopify CLI refuses to deploy an extension targeting a version older than 12 months.
The REST Admin API still exists but is legacy; build new work on GraphQL. Official libraries exist for Node, Ruby, PHP, Python and .NET.
Authentication
Public app, OAuth authorization code grant
- Redirect the merchant to
https://{shop}/admin/oauth/authorize?client_id={client_id}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}. Append&grant_options[]=per-userfor an online (per staff member) token. - Shopify redirects back to
redirect_uriwith acode. - Exchange it.
curl -X POST "https://{shop}/admin/oauth/access_token" \
-d "client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&code=${CODE}&expiring=1"
{
"access_token": "f85632530bf277ec9ac6f649fc327f17",
"scope": "read_products,write_orders",
"expires_in": 3600,
"refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token_expires_in": 7776000
}
expiring=1 opts into short-lived tokens with a refresh token. Without it, an offline token has no expiry and no refresh token. Offline tokens persist across sessions and are not tied to a staff member, which is what a background sync needs. Online tokens are scoped to the authorising staff member, expire with that session, carry associated_user and associated_user_scope, and have no refresh token, so they are unsuitable for background work. An app that needs both stores offline tokens keyed by shop and online tokens keyed by shop plus staff member.
- Call the API with the token in a header.
curl -X POST "https://{shop}/admin/api/2026-07/graphql.json" \
-H "X-Shopify-Access-Token: ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"query":"{ shop { name } }"}'
Useful scopes for a POS pipeline: read_orders, read_products, read_inventory, read_locations, read_fulfillments, read_customers, read_shopify_payments_payouts, plus write_inventory, write_products and write_publications for write-back.
Multi-account: one token per shop domain, keyed on {shop}.myshopify.com. A merchant with several physical shops is still one Shopify store and one token; the shops are Location records inside it.
Objects we can read
Orders, including the POS-specific fields
POST https://{shop}/admin/api/{version}/graphql.json, query orders(first:, after:, query:, sortKey:, reverse:). Default sortKey is PROCESSED_AT. Pagination is cursor based through edges { cursor node } and pageInfo { hasNextPage endCursor }.
The query argument is a search string, and the two filters that matter here are source_name and location_id. source_name distinguishes web, POS, draft orders and third party channels; location_id filters to the location associated with the order. Combine with updated_at for an incremental pull.
query RetailOrders($cursor: String) {
orders(first: 100, after: $cursor, query: "source_name:pos AND updated_at:>2026-09-01", sortKey: UPDATED_AT) {
edges {
cursor
node {
id
name
processedAt
updatedAt
sourceName
sourceIdentifier
displayFinancialStatus
displayFulfillmentStatus
retailLocation { id name }
staffMember { id name }
app { name }
publication { id name }
currentTotalPriceSet { shopMoney { amount currencyCode } }
cashRoundingAdjustment { paymentSet { shopMoney { amount currencyCode } } }
}
}
pageInfo { hasNextPage endCursor }
}
}
The POS-relevant fields on Order:
physicalLocation and channelInformation are deprecated; use retailLocation and publication.
Order items
Line items come back on the order as the lineItems connection, each with id, name, sku, quantity, variant { id sku }, originalUnitPriceSet, discountedTotalSet and taxLines. They are not a separately queryable root.
Products and listings
products(first:, after:, query:) and productVariants(...). A variant carries sku, barcode, price, compareAtPrice, inventoryItem { id } and inventoryQuantity. For POS, publication matters as much as price: a product has to be published to the Point of Sale channel to be sellable in a shop, and publication state is read through the publications query and the resource's publication fields.
The publishablePublish reference confirms the mutation and the PublicationInput.publicationId argument but does not explicitly name Point of Sale among supported publications, noting only that scheduled publishing is online-store-only. Before relying on it, run a publications query against a real store and read back the Point of Sale publication ID.
Inventory
location.inventoryLevels gives the quantities of inventory items stocked at that location, and inventoryItem links back to the variant. Named quantities include available and on_hand. For a retail dataset the per-location split is the whole point: one InventoryLevel row per item per shop.
Locations
The Location object is the shop. Fields: id, name (String!), address (LocationAddress!), addressVerified (Boolean!), isActive (Boolean!), fulfillsOnlineOrders (Boolean!), hasActiveInventory (Boolean!), shipsInventory (Boolean!, legacy), activatable, deactivatable, legacyResourceId (UnsignedInt64!), localPickupSettingsV2, suggestedAddresses, inventoryLevels, createdAt, updatedAt. Needs read_locations or read_inventory. Note that there is no boolean saying "this is a retail shop": infer it from whether orders carry it as retailLocation, or from fulfillsOnlineOrders and local pickup settings.
Returns and refunds
Refunds hang off the order (refunds, with refundLineItems and transactions), and the returns objects (Return, ReturnLineItem) model the merchandise return workflow. In-store returns therefore arrive as refunds on the original order, attributable to a staffMember and a location.
Payments and settlements
ShopifyPaymentsPayout is the closest thing to a settlement record and covers both card present and online takings where Shopify Payments is the processor. Fields: id, legacyResourceId, issuedAt, net, status, transactionType, externalTraceId, businessEntity, and summary broken down into chargesGross, chargesFee, refundsGross, refundsFee, adjustmentsGross, adjustmentsFee. bankAccount and gross are deprecated. Requires payout permission. If the merchant uses a third party processor for card present, there is no payout object.
Customers
customers(...) and order.customer, with email, phone, addresses and defaultAddress. All PII. POS lets staff attach a customer to a sale, so retail orders may or may not have one.
POS UI extensions
The only genuinely POS-specific build surface. Extensions are Preact TSX components configured by a TOML file, scaffolded and deployed with the Shopify CLI (shopify app deploy), and previewed on a development store through the real POS app. Targets fall into three shapes:
- Tiles on the home smart grid, for example
pos.home.tile.render. - Actions, menu items and full screen modals, for example
pos.product-details.action.menu-item.renderandpos.home.modal.render. - Blocks, inline content on existing screens, for example
pos.product-details.block.renderandpos.cart.line-item-details.action.render.
Constraints worth knowing before you plan work: the compiled bundle cannot exceed 64 KB (128 KB for full-page customer account extensions); extensions can make authenticated requests to your backend and, on POS 10.6.0 and later with API 2025-07 or newer, direct GraphQL Admin API queries; offline operation exists from POS 11.0 but only for locally available APIs such as Storage, Locale, Toast and Scanner, so anything network-dependent fails without connectivity; and credentials must never be hardcoded, because the bundle is client side and inspectable.
Writing back: listings, price and stock
inventorySetQuantities takes an InventorySetQuantitiesInput with a required name (available or on_hand), a required reason, and a quantities array of {inventoryItemId, locationId, quantity, compareQuantity}, plus optional ignoreCompareQuantity and referenceDocumentUri. compareQuantity gives you compare-and-set against concurrent writes. Shopify's own guidance is to use this mutation only when your system is the source of truth for stock, and inventoryAdjustQuantities otherwise. Idempotency keys are required as of API version 2026-04.
{
"input": {
"name": "available",
"reason": "correction",
"quantities": [
{ "inventoryItemId": "gid://shopify/InventoryItem/30322695",
"locationId": "gid://shopify/Location/124656943",
"quantity": 11, "compareQuantity": 1 }
]
}
}
The response returns an inventoryAdjustmentGroup with reason and changes { name delta }, so you can confirm both available and on_hand moved as expected, plus userErrors. Note that GraphQL mutations return errors in userErrors with an HTTP 200; a connector that only checks status codes will silently drop failures.
Webhooks and notifications
Subscriptions are declared either in shopify.app.toml or created through the GraphQL Admin API, and delivered to an HTTPS URL, a Google Pub/Sub URI or an Amazon EventBridge ARN. Deliveries carry X-Shopify-Topic, X-Shopify-Webhook-Id, X-Shopify-Triggered-At and X-Shopify-Hmac-Sha256, the last of which you verify against the raw request body using the app's client secret.
For POS the useful topics are the ordinary order and inventory ones (orders/create, orders/updated, orders/cancelled, refunds/create, inventory_levels/update, products/update). There is no POS-only topic; filter on source_name in the delivered body.
The retry count and window are not stated on the overview page read here. What the documentation does say plainly is that delivery is not guaranteed and that apps should not rely on webhooks alone: run a background reconciliation job that queries with an updated_at filter. Do that regardless.
Rate limits and pagination
GraphQL is cost based. Every query has a calculated cost in points, deducted from a bucket that refills continuously. Published restore rates: 100 points per second on Standard, 200 on Advanced Shopify, 1000 on Shopify Plus, and 2000 on Shopify for enterprise (Commerce Components). The bucket size varies by plan and is not stated numerically in the reference page read; read it at runtime instead, from the extensions.cost.throttleStatus block on every response, which carries maximumAvailable, currentlyAvailable and restoreRate.
Practical consequence: the limit is on query cost, not request count, so a connector controls its own throughput by trimming the selection set. Asking for lineItems(first: 250) inside orders(first: 250) will blow the bucket; page orders at 50 to 100 and fetch line items in the same query with a modest first.
Pagination is uniformly cursor based on GraphQL: pass after: endCursor until hasNextPage is false. Cursors are opaque; sort by UPDATED_AT and checkpoint the timestamp so a restart does not depend on a stale cursor.
Mapping to the unified model
Gaps and open questions
- There is no POS API, no POS-only webhook topic and no "retail sale" object. Everything is attribution on the standard order model, so a connector that does not filter
sourceNamewill blend channels. - The exact
sourceNamevalue for POS is documented by example rather than as an enumeration, so confirm the literal string on a live store before hardcodingsource_name:pos. Likewise, whether Point of Sale appears amongpublicationsforpublishablePublishis not stated on the mutation reference; verify with apublicationsquery. - GraphQL bucket sizes per plan are not published numerically, only restore rates, so read
throttleStatusat runtime. Webhook retry count and window were not stated on the page read either. - No
Locationfield identifies a retail shop, so shop versus warehouse has to be inferred or configured. - POS Pro pricing and the list of countries where card present payments are supported were not verified for this note.
Sources
- Admin GraphQL API, Order object
- Admin GraphQL API, orders query
- Admin GraphQL API, Location object
- Admin GraphQL API, inventorySetQuantities mutation
- Admin GraphQL API, publishablePublish mutation
- Admin GraphQL API, ShopifyPaymentsPayout object and Authorization code grant
- Webhooks overview, GraphQL Admin API rate limits and POS UI extensions