Hi all,
I’m building a custom rug configurator on top of a Shopify theme (Online Store 2.0 sections, no headless setup). Here’s the flow:
- A storefront page lets the customer upload a reference/design image via a plain .
- That image, along with size/shape/material choices collected across a couple more pages, gets passed forward (via localStorage between pages) to a final “Review Request” page styled like Checkout.
- On submit, all the data (customer info + product config + image) is sent to Shopify’s native /contact form endpoint, and separately to a webhook that a Mechanic (Shopify Flow-style app) task listens to.
- The Mechanic task runs a draftOrderCreate GraphQL mutation, attaching the image reference and config as customAttributes on the draft order line item, so our team can see everything in one place in the Shopify admin.
Originally we stored the image as a Base64 data URL the whole way through, which bloated every step (localStorage, the contact form email, and the webhook payload) and made the draft order UI painfully long to scroll through.
We’ve since fixed the bloat by switching to a third-party image host (uploading directly from the browser to an unsigned upload endpoint) and passing only the short returned URL through the rest of the flow, then displaying that URL as a custom attribute value on the draft order. That part works fine now.
What I actually want, though, is to skip the third-party host entirely and upload the customer’s image directly into Shopify’s own File system (Settings > Files / the fileCreate Admin API mutation), so the image lives natively in Shopify and I get a myshopify/cdn.shopify.com URL back — the same way product images work — instead of relying on an external service.
The blocker I see: fileCreate is an Admin API mutation, which needs an Admin API access token — and I obviously can’t expose that token in storefront JavaScript. So a direct browser → Admin API call isn’t safe.
What’s the current recommended, secure way to do this in 2026?
I’ve found some older forum threads referencing endpoints that seem to be deprecated or app-only now, so I’d really appreciate pointers to current, working documentation or a working example of this exact “customer uploads an image on the storefront → it lands in Shopify Files → the URL gets used in a Draft Order” flow.
Thanks in advance — happy to share more of my current setup if it helps someone give a precise answer.
Hi @vedant_shopify_dev Welcome To Shopify Community So Since you already have Mechanic in the loop listening to a webhook, that’s actually your secure bridge here, you don’t need a separate third-party host at all. Instead of the browser calling fileCreate directly (which would expose the Admin API token, correctly identified as unsafe), route the image upload through your existing Mechanic task instead of straight to the third-party host: have the browser POST the image (or a signed upload) to the same webhook endpoint Mechanic already listens to, then inside that Mechanic task, call the fileCreate GraphQL mutation server-side (Mechanic tasks run with your store’s Admin API access, so the token never touches the browser). fileCreate returns the file’s resource URL, which you then attach as the custom attribute on the draftOrderCreate mutation, same as you’re doing now with the third-party host’s URL, just swapping the source.
The key architectural point: fileCreate is asynchronous, it returns a file object with a status of UPLOADED that needs a follow-up fileUpdate or a webhook/polling check (files/update webhook topic) since the CDN URL isn’t always immediately ready on the same request, Shopify processes the file after creation. If you need the URL immediately for the next step in your flow (like passing it to the Review Request page), you may need your Mechanic task to poll briefly or handle the files/update webhook to catch the final CDN URL once processing finishes, rather than assuming it’s returned synchronously and ready to use right away. This keeps your existing architecture (browser → webhook → Mechanic → draft order) almost identical to what you have now, you’re just relocating the upload destination from a third-party host to Shopify Files, with Mechanic acting as the secure server-side intermediary that already exists in your flow. Hope this helps solve your problem, and if it does, don’t forget to like and mark it as the solution. Thank you!
The short version : you’re right that you can’t call fileCreate straight from the browser since it needs an Admin API token. The trick is to let your storefront upload the image somewhere temporary and safe, then have your backend (in your case, Mechanic) turn that into a permanent Shopify file. Two steps :
- Get a temporary upload slot. Your backend calls Shopify’s
stagedUploadsCreate mutation, which hands back a one-time upload URL. This step needs the Admin API token, but it happens server-side, so nothing sensitive touches the customer’s browser.
- Upload the image to that URL. The browser sends the file straight to the temporary URL Shopify gave you - no token needed for this part.
- Finalize it as a real file. Once the upload lands, your backend calls
fileCreate with the temp file’s URL, and Shopify gives you back a permanent cdn.shopify.com link you can attach to the draft order.
A couple of things that trip people up:
fileCreate runs asynchronously, so the CDN URL isn’t ready instantly - you’ll need to poll (or listen for the files/update webhook) until the file status flips to “ready” before you use the URL.
- Since Mechanic already runs with Admin API access, it’s a natural place to do both the staged upload request and the
fileCreate call - your storefront never needs to know an Admin token exists.
This avoids the Base64 bloat and skips the third-party host entirely, while keeping your Admin credentials off the client. Hope that helps! 
Since you already have Mechanic in your flow, you can bridge the Admin API gap securely without exposing tokens in the browser:
- Send payload to Mechanic: Post the image data directly from storefront JS to your existing Mechanic webhook endpoint.
- Staged upload target: Have Mechanic execute the
stagedUploadsCreate GraphQL mutation (resource: "FILE") to generate a temporary Shopify target URL and parameters.
- Upload binary: Send the raw image file directly to that temporary staging URL.
- Create native file: Call the
fileCreate Admin API mutation inside Mechanic using the staged upload target key to register it into Settings > Files.
- Attach CDN URL to Draft Order: Once
fileCreate finishes, retrieve the cdn.shopify.com URL and pass it as a line item custom attribute in your draftOrderCreate mutation.
Note: fileCreate processes asynchronously, so listen to the files/update webhook or poll briefly in Mechanic if the CDN URL is not instantly available.
Worth checking whether you need the Admin API for this at all. Shopify’s cart accepts file uploads natively: an <input type="file" name="properties[Design]"> inside the product form uploads to Shopify’s own CDN and lands as a link on the line item, with no token, no third party host and no staged upload step.
The catch is that it is tied to the cart, so it fits a configurator that ends in add to cart rather than one that ends in a contact form and a draft order. If the draft order route is fixed for you then the staged upload answers above are right, but the reframe is worth ten minutes because it removes a whole moving part from the flow. 