Key Takeaways
- An HTML 403 saying "Just a moment..." is Cloudflare, not your access token
- No x-request-id header means the request never reached Shopify's app
- Log the CF-RAY ID and timestamp of every failed call
- Shopify checks store-specific logs only for staff or collaborators
- Retry challenged calls in the same run, not on the next trigger
A pipeline where 15 minutes matter
We run an order automation for a Dutch e-commerce brand that ships personalised gifts with fast delivery. Twice every weekday, at 8:00 and 14:00 Amsterdam time (13:00 on Fridays), it pulls the latest orders from the Shopify Admin REST API. It then builds a packing slip PDF for each order, merges them into one file and emails the batch to the warehouse.
The data has to be fetched at send time. Orders get edited right up to the cutoff, and a packing slip built from an old version of an order is a wrong shipment. So there is no "prepare it earlier" option: if the API call fails at 14:00, the batch is late.
The whole thing runs on Google Apps Script with time-driven triggers, talking to Shopify through a custom app. It ran without a problem for over a year.
Random 403s that came and went
In early June 2025, some of our API calls started failing with 403 Forbidden. Not all of them: the same request would succeed several times in a row, then fail, then succeed again.
Nothing had changed on our side. Same access token, same scopes, same API version (REST 2025-01). We were sending a handful of requests per hour, far below Shopify's rate limits. The failures got more frequent through June and July, stopped on 26 July, then came back on 3 August.
A 403 usually means a permissions problem. Ours were fine, and a permissions problem doesn't fix itself between two runs.
Our workflow only marks a batch as sent once it succeeds, and the trigger fires every 15 minutes. So a failed run was retried automatically, and no batch was lost. But every failure cost at least 15 minutes, on a business that promises fast delivery. The client felt every one of them.
Did You Know?
Requests to *.myshopify.com pass through Cloudflare before they reach Shopify. When Cloudflare suspects a bot, it can answer with a challenge page titled "Just a moment..." instead of passing the request on. A browser solves that challenge with JavaScript and cookies. A script calling an API can't, so all it sees is a 403.
How we proved it wasn't our code
We logged the full request and response of every call. When a call failed, this is what came back (token and store domain redacted):
GET /admin/api/2025-01/products.json?limit=250 HTTP/1.1
Host: your-store.myshopify.com
X-Shopify-Access-Token: [REDACTED]
Accept: application/json
HTTP/1.1 403 Forbidden
Server: cloudflare
CF-Mitigated: challenge
CF-RAY: 969935ad2c0e478c-DFW
Content-Type: text/html; charset=UTF-8
Date: Sun, 03 Aug 2025 22:21:43 GMT
<title>Just a moment...</title>
...
<span id="challenge-error-text">Enable JavaScript and cookies to continue</span>Four things in that response point away from our code:
Server: cloudflareandCF-Mitigated: challenge. Cloudflare answered, and it says it challenged the request.An HTML page instead of JSON. A real Admin API response is JSON. This was Cloudflare's "Just a moment..." challenge page.
No
x-request-idheader. Shopify's support team confirmed what this means: the request never reached Shopify's application. It was stopped at the edge.It was intermittent. The same request, with the same token, succeeded before and after each failure. A wrong token or a missing scope fails every time.
So the request left our script correctly and was blocked before Shopify ever saw it. Nothing we changed in our code would have fixed that.
25 days to escalate it
Knowing the cause didn't fix it. Cloudflare sits in front of Shopify, so only Shopify could change how it treated our calls. We opened a ticket with Shopify Developer Support on 31 July 2025.
31 Jul: We open the ticket with the error, request and response headers, and the dates.
1 Aug: First reply: Cloudflare is flagging the script's requests. Support suggests checking our server's Cloudflare setup and testing in Postman.
4 Aug: We point out that the Cloudflare in question is Shopify's own, and ask for escalation with the CF-RAY ID.
6 Aug: We send steps to reproduce plus the full headers. Support asks for the
x-request-id.8 Aug: We send every header we received. There is no
x-request-id.11 Aug: Support agrees: no
x-request-idmeans the request never reached Shopify. They ask which app makes the calls.19 Aug: Support needs us added to the store as staff or a collaborator before checking store-specific logs.
21 Aug: The client adds us. We send three CF-RAY IDs with exact timestamps, from 7 July, 3 August and 16 August.
25 Aug: Shopify's development team fixes it for our Apps Script calls.
Three things moved the ticket forward:
Evidence, not insistence. Headers, CF-RAY IDs and timestamps are things support can look up. "It fails randomly" is not.
Explaining why Postman couldn't help. The error was random, so one successful Postman call proves nothing. Our own script often succeeded several times in a row before failing.
Store access. Shopify only looks at store-specific logs with someone who is staff or a collaborator on that store. If you work for a client, get that access before you open the ticket.
The fix
On 25 August 2025, Shopify Developer Support confirmed the fix in writing:
"I've collaborated internally with our development team and this issue has been fixed for your Google Apps Script API calls."
From then on, Cloudflare stopped rejecting our requests.
We haven't seen a single one of those 403s since, more than 15 months later.
We also run other Apps Script integrations with Shopify for other clients, and they had been getting the same random 403s. Because those jobs aren't time-sensitive, simple retries were covering it up. The errors stopped there too, at the same time. Shopify only confirmed the fix for our integration, but from what we saw, it reached further.
Credit to Shopify's support team for staying on it and getting the right people involved once the evidence was clear.
What we'd add today
Our workflow did recover: a failed run was picked up by the next 15-minute trigger. But a Cloudflare challenge is usually gone a few seconds later, so waiting 15 minutes threw time away. If we built this again, we'd retry the call inside the same run, and only for this specific error:
// Retries a Shopify call only when Cloudflare challenged it,
// and logs the CF-RAY ID that Shopify support will ask for.
function fetchShopify(url, params, maxAttempts = 3) {
const options = Object.assign({}, params, { muteHttpExceptions: true });
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = UrlFetchApp.fetch(url, options);
const code = res.getResponseCode();
if (code === 200) return JSON.parse(res.getContentText());
const headers = res.getAllHeaders();
const header = name => {
const key = Object.keys(headers).find(k => k.toLowerCase() === name);
return key ? headers[key] : null;
};
const challenged = code === 403 && header('cf-mitigated') === 'challenge';
console.warn(`Attempt ${attempt}: HTTP ${code}, CF-RAY ${header('cf-ray')}, ${new Date().toISOString()}`);
if (!challenged || attempt === maxAttempts) {
throw new Error(`Shopify request failed with HTTP ${code}`);
}
Utilities.sleep(5000 * attempt); // wait 5 s, then 10 s
}
}Two details matter here. It retries only challenged 403s: a real permissions error should fail fast, not three times. And it logs the CF-RAY ID with a UTC timestamp, which is exactly what Shopify support asked us for.
Frequently Asked Questions
Does a 403 from the Shopify Admin API always mean missing scopes?
Why couldn't we reproduce it in Postman?
What should I send Shopify support?
Should I just retry?
If you have questions, contact us. Email at support@autobizz.com.


