We write a lot of code that pushes prices, SKUs and barcodes into Shopify from other systems, and the ID format is where most first attempts go wrong. So here's the short version we wish we'd had.
The format
Shopify's GraphQL APIs (Admin, Storefront and Customer Account) all use global IDs. The pattern is gid://shopify/{object}/{id}, so:
- Variant:
gid://shopify/ProductVariant/43729076 - Product:
gid://shopify/Product/20995642 - Inventory item:
gid://shopify/InventoryItem/123
Two things catch people out. The object name is case-sensitive and singular: ProductVariant, not productvariant or Variant. And it's a string, not a number, so don't let your language or your database helpfully turn it into one.
A few objects use a longer, parameterised form because they need a parent's ID too. Inventory levels are the one you'll meet, for example gid://shopify/InventoryLevel/123?inventory_item_id=456. Variants don't do this. A variant GID is always the simple form.
Converting between the REST ID and the GID
From numeric ID to GID you can just build the string. If you're reading from the REST Admin API, you don't even need to: every REST resource includes an admin_graphql_api_id property holding the GID for you.
From GID to numeric ID, the safe way is to ask for legacyResourceId, which Shopify describes as "the ID of the corresponding resource in the REST Admin API":
query GetProductVariant($id: ID!) {
productVariant(id: $id) {
id
legacyResourceId
sku
price
}
}
In practice most of us also strip the prefix in code when we already have the GID in hand. That's fine for variants, as long as you handle the parameterised IDs separately if you ever reuse the same helper elsewhere. Here's what we use in PHP:
function variant_gid($id) {
$id = (string) $id;
if (strpos($id, 'gid://shopify/ProductVariant/') === 0) {
return $id;
}
if (!ctype_digit($id)) {
throw new InvalidArgumentException("Not a variant ID: $id");
}
return 'gid://shopify/ProductVariant/' . $id;
}
function variant_numeric_id($gid) {
if (!preg_match('~^gid://shopify/ProductVariant/(\d+)$~', $gid, $m)) {
throw new InvalidArgumentException("Not a variant GID: $gid");
}
return $m[1]; // keep it as a string: these IDs outgrow 32-bit integers
}
And the same idea in JavaScript:
const variantGid = (id) =>
String(id).startsWith('gid://shopify/ProductVariant/')
? String(id)
: `gid://shopify/ProductVariant/${id}`;
const variantId = (gid) => gid.split('/').pop(); // a string, on purpose
That last comment matters. Variant IDs are 64-bit, and Shopify types legacyResourceId as an unsigned 64-bit integer. JavaScript numbers lose precision past about 9 quadrillion, and plenty of older PHP setups and spreadsheets mangle long numbers too. Keep them as strings end to end and you'll never chase a variant that's off by a few hundred.
Where you'll find the numeric ID
- The REST Admin API, as
id, with the GID alongside asadmin_graphql_api_id. - The admin, as the number at the end of a variant's URL when you open it from the product page.
- Liquid and the storefront, as
variant.idand in links like?variant=43729076. - The GraphQL Admin API, as
legacyResourceIdon the variant.
The Storefront API used to encode them
If you've got old code with base64_decode or atob wrapped round variant IDs, this is why. The Storefront API used to return its IDs Base64-encoded, so a variant came back as an unreadable string that decoded to the gid:// form.
Shopify stopped that from Storefront API version 2022-04, which was the first stable version to return plain GIDs. Encoded IDs as arguments were deprecated at the same time, and Shopify said support for them would be removed in 2023-07. Every supported version now uses the same plain gid://shopify/ProductVariant/... string as the Admin API. If you stored encoded IDs in a database years ago, decode them once and get rid of the encoding code.
Using the GID to update a variant today
This is where our old post came from. Shopify deprecated the single-variant mutations in favour of the bulk ones as of Admin API version 2024-10, with productVariantsBulkUpdate replacing productVariantUpdate. The old mutation isn't in the current reference at all (I checked 2026-10). If an integration started failing on variant updates, that's usually why.
The bulk mutation needs the parent product's GID as well as each variant's:
mutation UpdateVariants($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants {
id
sku
price
compareAtPrice
}
userErrors {
field
message
}
}
}
{
"productId": "gid://shopify/Product/20995642",
"variants": [
{
"id": "gid://shopify/ProductVariant/43729076",
"price": "49.00",
"compareAtPrice": "65.00",
"inventoryItem": { "sku": "TSHIRT-BLK-M" },
"barcodes": [ { "value": "5012345678900" } ]
}
]
}
A few things worth knowing from the 2026-10 reference:
- SKU lives on the inventory item when you write it. You read
skustraight off the variant, but you set it throughinventoryItem: { sku: ... }. - Barcode has changed. The single
barcodefield is deprecated. Usebarcodes, a list of up to 20 values, which replaces the whole set each time. - It's all or nothing by default. With
allowPartialUpdatesleft atfalse, one bad variant stops the whole batch. Always readuserErrors, because a request can come back with a 200 and still have changed nothing. - You need the
write_productsscope on your app.
The short version
Build the GID as gid://shopify/ProductVariant/{id}, get the number back with legacyResourceId, keep both as strings, forget about Base64, and send updates through productVariantsBulkUpdate with the product's GID alongside. That covers almost every variant ID problem we've seen.
If you've got a stock system, ERP or price feed that needs to talk to Shopify properly, that's a lot of what we do. Have a look at our custom Shopify development and API and integration work, or see how we built ShopFlow to run stock and orders on top of Shopify.
Cheers, Rob