Skip to main content

POS UI extensions

The UI Extensions library enables individuals to build extensions that use interface elements and behaviors that mirror the look and feel of the POS retail experience. These elements render natively, providing the performance and accessibility inherent to a native app. POS UI extensions are available for the smart grid.

Tip

Shopify constantly works on adding new features to POS UI extensions. You can visit the changelog to make sure you're using the latest version of POS UI extensions.

POS UI extensions can also make authenticated calls to your app's backend. When you use fetch() to make a request to your app's configured auth domain or any of its subdomains, an Authorization header is automatically added with a Shopify OpenID Connect ID Token (formerly known as a Session Token). There's no need to manually manage ID tokens.

Relative URLs passed to fetch() are resolved against your app's app_url. This means if your app's backend is on the same domain as your app_url, you can make requests to it using fetch('/path').

If you need to make requests to a different domain, you can use the session.getSessionToken() method to retrieve the ID token and manually add it to your request headers.

Important: ID tokens are only returned for authenticated users who are permitted to use your app. When the authenticated user (the user that logged into Shopify POS with their email address) doesn't have the correct app permission enabled for your app, the token will be null. This is irrelevant of which POS Staff member is pinned in, as those are not authenticated users. For more information on configuring app permissions, see the Shopify app permissions documentation.

Note

App Authentication is available as of POS version 10.6.0 for extensions targeting 2025-07 or later.

Make requests to your app's backend

import {
reactExtension,
useApi,
Text,
POSBlock,
POSBlockRow,
} from '@shopify/ui-extensions-react/point-of-sale';
import {useEffect, useState} from 'react';

const CustomerDetailsBlock = () => {
const {customer} = useApi<'pos.customer-details.block.render'>();
const [loyaltyInfo, setLoyaltyInfo] = useState<any>();
useEffect(() => {
getLoyaltyInfo();
}, [customer.id]);

async function getLoyaltyInfo() {
const res = await fetch(`${URL}/api/loyalty/${customer.id}`);
const json = await res.json();
setLoyaltyInfo(json.loyaltySummary);
}

return (
<POSBlock>
<POSBlockRow>
<Text>{loyaltyInfo}</Text>
</POSBlockRow>
</POSBlock>
);
};

export default reactExtension('pos.customer-details.block.render', () => (
<CustomerDetailsBlock />
));

You can make Shopify Admin API requests directly from your extension using the standard web fetch API!

Any fetch() calls from your extension to Shopify's Admin GraphQL API are automatically authenticated by default. These calls are fast too, because Shopify handles requests directly.

Direct API requests use online access mode by default.

Note

Direct API access is available as of POS version 10.6.0 for extensions targeting 2025-07 or later.

Access scopes

Be sure to declare all required access scopes in your app's TOML file. For local development, access scopes are only registered or updated when the app is deployed and installed on your test store.

Query Shopify data directly

import {
reactExtension,
useApi,
POSBlock,
POSBlockRow,
Text,
} from '@shopify/ui-extensions-react/point-of-sale';
import type {DirectApiRequestBody} from '@shopify/ui-extensions/point-of-sale';
import React, {useEffect, useState} from 'react';

// This mutation requires the `write_products` access scope.
// https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldsset
async function mutateMetafield(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
key
namespace
value
createdAt
updatedAt
}
}
}
`,
variables: {
metafields: [
{
key: 'direct_api',
namespace: 'custom',
ownerId: `gid://shopify/Product/${productId}`,
value: 'Example Value',
},
],
},
};

await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
}

// https://shopify.dev/docs/api/admin-graphql/latest/queries/product
async function queryProductMetafields(productId: number) {
const requestBody: DirectApiRequestBody = {
query: `#graphql
query GetProduct($id: ID!) {
product(id: $id) {
id
metafields(first: 10) {
edges {
node {
id
namespace
key
value
}
}
}
}
}
`,
variables: {id: `gid://shopify/Product/${productId}`},
};
const res = await fetch('shopify:admin/api/graphql.json', {
method: 'POST',
body: JSON.stringify(requestBody),
});
return res.json();
}

const ProductDetailsBlock = () => {
const {product} = useApi<'pos.product-details.block.render'>();
const [productInfo, setProductInfo] = useState<string>('');
useEffect(() => {
async function getProductInfo() {
const result = await queryProductMetafields(product.id);
setProductInfo(JSON.stringify(result, null, 2));
}
getProductInfo();
}, [product.id]);

return (
<POSBlock>
<POSBlockRow>
<Text>Metafields: {productInfo}</Text>
</POSBlockRow>
<POSBlockRow onPress={() => mutateMetafield(product.id)}>
<Text>Set metafield</Text>
</POSBlockRow>
</POSBlock>
);
};

export default reactExtension('pos.product-details.block.render', () => (
<ProductDetailsBlock />
));