Skip to content

Claude Code Shopify DevelopmentStep 2 of 3

Automating Shopify With the Admin API and Claude Code

Anything you can do in the Shopify admin you can do through the API — and a change made through the API has history, review and a way back. This covers the parts that bite: userErrors returning HTTP 200, publishing being separate from creating, and what the API cannot see.

Claude Code Guides: Automating Shopify With the Admin API. A client node labelled "you" connected to three resource cards, one link highlighted.

Why the API and not the admin

Anything you can do in the Shopify admin, you can do through the Admin API. The reason to prefer the API is not speed. It is that a change made in the admin has no history, no review, and no way to be repeated on another store or rolled back on this one.

A product description typed into a textarea exists in exactly one place: someone else's database. Move it to a file in your repository and publish it with a script, and it becomes a diff. You can see what changed, when, and why. You can run a check before it ships. Claude Code can read it.

That is the whole argument, and it is the same argument as for infrastructure as code. The API is how you get there.

This assumes you already have a store and a theme. If you do not, building a Shopify store with Claude Code covers the theme workflow and the platform constraints first, and how to build a website with Claude Code covers the parts that are not Shopify-specific.

Getting access without leaking it

Create a custom app in Settings → Apps and sales channels → Develop apps, choose its Admin API scopes, install it, and reveal the access token once. Shopify shows it a single time.

Where that token goes is the decision that matters, and the wrong answer is expensive because a leaked Admin token is not a password — it is your whole store, including customers and orders.

Keep it in a file outside the repository and read it by path. Not in .env at the project root where a careless git add -A can catch it. Not in a CI variable you also echo in a debug step. Outside the tree entirely, referenced by an environment variable that holds the path rather than the secret:

export SHOPIFY_TOKEN_PATH=/opt/shopify-admin-token.txt

Then tell Claude Code the rule explicitly, because it will not infer it:

Credentials live outside this repository and are read by path from
an environment variable. Never print a token, never write one into a
file in the tree, never include one in a commit, a log line, a
generated document, a screenshot, or an error message.

If diagnostic output would contain a credential, redact it before
printing.

That last sentence earns its place. The common leak is not a committed secret; it is a stack trace or a debug dump of a request that happens to include the header.

Rules like this belong in CLAUDE.md rather than in the prompt you happen to be writing, so they apply to every session without being restated. A production CLAUDE.md for web development covers what belongs in that file and, more usefully, what does not.

Add a check that fails the build if a token-shaped string appears anywhere in the tracked tree. Shopify tokens have recognisable prefixes, which makes the check cheap to write and worth having before you need it.

One client, and why it matters

Write one small module that owns the HTTP call and have every script import it. Not because it saves lines — it does not, at first — but because everything you will later want to be true of every request has exactly one place to live: the API version, the token, the timeout, retry on throttling, and the redaction that stops a token reaching a log.

Pin the API version explicitly. Shopify releases quarterly and supports each version for a year, and an unpinned client is a script that breaks on a date you did not choose:

https://your-store.myshopify.com/admin/api/2025-07/graphql.json

Prefer GraphQL over REST for new work. Shopify has been moving functionality to GraphQL for years and some of it exists nowhere else — publications and staged uploads among them.

Creating a product that is actually correct

A digital product is not a physical one with the shipping switched off, and the fields that make the difference are not on the product. They are on the variant's inventory item:

Field Value for a download What goes wrong otherwise
requiresShipping false Checkout asks for a shipping address and may add a rate
tracked false The product sells out after one order
weight 0 Weight-based rates apply to a file
sku Set it Order exports and reconciliation have nothing to join on

Those live under productVariantsBulkUpdate, not productCreate. Creating the product and never touching the variant is the single most common way a digital product ships wrong, and nothing about the storefront looks unusual until someone reaches checkout.

Ask for the fields back in the mutation response and print them. A script that reports what the API returned, rather than that it did not throw, is the difference between knowing and assuming:

After every write, request the fields you set in the mutation's
response and print the returned values, not the values you sent.

Report success only on the basis of what came back.

userErrors: the failure that returns HTTP 200

This is the one that catches everyone.

A GraphQL mutation that fails validation returns HTTP 200 with an empty result and a populated userErrors array. If your script checks the status code, it will report success while having changed nothing at all.

{
  "data": {
    "productCreate": {
      "product": null,
      "userErrors": [
        { "field": ["handle"], "message": "Handle has already been taken" }
      ]
    }
  }
}
Three rows sharing one HTTP 200 response. The first is the request. The second returns a null product with a populated userErrors array and changed nothing. The third returns a product id with an empty userErrors array and wrote. Only the array distinguishes them.
Both outcomes are an HTTP 200. Only userErrors tells them apart.

Every mutation must have its userErrors checked and must stop on a non-empty array. Put it in the shared client so it cannot be forgotten, rather than in each script where it will be:

Every GraphQL mutation returns userErrors. Check it after every
mutation and raise on a non-empty array — a mutation that failed
validation returns HTTP 200 and changes nothing.

Do not treat a 200 as success.

Then prove the check works by sending a mutation you know is invalid and watching it stop. A guard nobody has seen fire is a guard you are trusting on faith.

The prompts above are deliberately terse. The prompt patterns that hold up in web development covers why a constraint stated once in a session decays, and what to do about it.

Publishing, which is separate from creating

A product created through the API is ACTIVE and invisible. Status and publication are different things: status says whether the product is live, and publication says which sales channels carry it. A new product is published to none of them.

Publishing is publishablePublish, and it needs the publication's id, which you query for. Expect at least an Online Store publication; a store with the Shop channel or Point of Sale installed will have more.

The symptom when you forget is distinctive and confusing: the product resolves in the admin, the API returns it happily, and the storefront 404s. If a product you just created cannot be found on the site, check publications before you check anything else.

Metafields, and letting the theme read live data

Metafields are where structured data belongs when it is not a native field: which topic hub an article belongs to, the ordered steps of a learning path, an SEO title that differs from the display title.

Two things are worth knowing before you design around them.

Shopify rejects a blank metafield value outright. You cannot set a field to empty to mean absent. Omit it instead, and read it in Liquid with != blank, which handles a missing metafield as nil. Trying to write "" gets you a validation error at the worst possible moment, in a bulk write, halfway through.

The type is part of the identity. number_integer and single_line_text_field are different fields even with the same key, and changing a type after the fact means deleting and recreating. Decide types once, deliberately.

The payoff is that the theme reads live values. A price rendered from product.price cannot go stale; a price typed into a section setting can, and will, on the day you run a sale.

Files and images: staged uploads

Uploading a file is three calls, not one, and the shape surprises people: ask Shopify for a staged upload target, POST the file to that target with the parameters it gave you, then reference the resulting URL in a fileCreate or productCreateMedia.

Two practical notes. The parameters from step one must be sent in the order given and before the file part, because the target is S3-compatible and cares. And media is processed asynchronously: productCreateMedia returns UPLOADED, not READY. A script that immediately asserts the image is live will fail intermittently, which is the worst kind of failure to debug. Poll for status, or accept that the assertion belongs in a later step.

Writing scripts you can run twice

Every publishing script should be safe to run again. Not as a nicety — because you will run it again, usually while something is half-finished and you are not certain what got through.

The pattern is find-then-create-or-update, keyed on the handle:

Every publish script must be idempotent, keyed on the handle:
query for the resource, update it if it exists, create it if it
does not. Never create unconditionally.

Print which of the two happened.

That last line matters more than it looks. A script that says updated when you expected created has just told you something true about the store that you did not know.

The same applies to a run that dies halfway. If each step is independently idempotent, recovery is running the script again. If it is not, recovery is reading the code to work out where it stopped.

Guards worth writing before you need them

Some mistakes are cheap to prevent and expensive to make. These are worth putting in the tooling rather than in your memory:

  • Refuse to write to the published theme. Theme pushes should target a development theme and fail loudly against the live one, with an override that has to be set deliberately.
  • Refuse to upload the paid product to the CDN. Files uploaded through the Files API get a public URL, which bypasses checkout entirely. A filename check with no override flag is four lines and prevents an unrecoverable mistake.
  • Never replace the generated sitemap. Shopify maintains it in real time. A hand-written replacement is stale the moment anything changes.
  • Assert counts you assert publicly. If a product page claims a number of files, have the publish script count them and refuse to publish when the claim and the artefact disagree.

The last one generalises. Any number that appears in two places will eventually disagree; the question is whether you find out from a check or from a customer.

Rate limits are a cost model, not an error

The GraphQL Admin API uses a cost-based leaky bucket rather than a request count. Each query has a cost, the bucket refills at a fixed rate, and an expensive query costs more than a cheap one regardless of how many you send.

Every response carries the current state under extensions.cost, including the points remaining and the restore rate. Read it rather than guessing: a client that backs off on the actual remaining budget is both faster and better behaved than one that sleeps a fixed interval between calls.

Handle THROTTLED by waiting and retrying, in the shared client, once. Requesting fewer fields is usually the better fix — cost scales with what you ask for, and most scripts ask for far more of the object than they use.

What the API cannot tell you

Worth knowing before you build a check on top of it.

Whether a digital delivery app has a file attached. Third-party apps store their data privately, and the Admin API cannot see it. A product can be active, priced, published and completely undeliverable, and every API-based check will call it healthy. The only proof is a real paid order that gets fulfilled.

That is not a gap you can close with better tooling, and pretending otherwise is how a store sells something it cannot deliver. Write the check so it fails honestly — "no fulfilled paid order proves delivery" — rather than passing on a technicality.

More broadly: the API tells you what the store is configured to do, and almost nothing about what a customer experiences. Confirm the storefront by fetching the storefront — which is also where the SEO workflow starts, for the same reason: what the template intends and what the response body contains are different things, and only one of them is what search engines and customers get.

Read these next

See how this fits into Claude Code Shopify Development

Continue your learning path

  1. Next

    What Shopify already does well, what is genuinely left to you, and the thin pages the platform creates without being ...

The whole sequence: Claude Code Shopify Development

Free download

The CLAUDE.md Starter Kit, free

Four working CLAUDE.md files you can drop into a project today, plus the one-page checklist for what belongs in one and how to tell whether yours is actually working.

  • CLAUDE.md for a static marketing site
  • CLAUDE.md for a web application, with security and migration rules
  • CLAUDE.md for a Shopify theme, including the gotchas that cost hours
  • CLAUDE.md for a shared package in a monorepo
  • A one-page checklist, and how to test the file is actually working
What are you working on?

The download appears here as soon as you submit. I will also email you when there is a new guide worth reading. No fixed schedule, no selling your address, unsubscribe from any email. See the privacy policy.