Shopify Cart API - a basic feature available on all Shopify online store storefronts is directly accessible by all users (humans, bots, scrapers, scripts…). It doesn’t require any authentication to be used.
Lately we’ve noticed huge spikes of carrier service rate request towards our App. The requests come from Shopify, so they are treated with trust. The requests are created abusing Shopify Cart API. The requests don’t have more shipping address information than country or country + zip code (in Shopify checkout you’ll need street address, too). In this case, it seems that only the carrier service requests are being scraped, as the same product’s are used in the cart to generate the requests, but the zip code / countries change.
I’ve tested this with a simple curl script. It is very easy to create a script to abuse this. For reasons such as:
- Data scraping for available shipping rates for each corner of the world
- Data scraping to replicate storefronts with similar product+shipping details
- Doing harm with traffic spikes to Apps utilizing carrier service
- Abuse store analytics (there’s been more discussion around this)
Any ideas how to approach this problem with an App offering carrier service rates? Cache helps a little, but it’s very easy to create a script to change the needed details to skip it. We wouldn’t want to limit real shipping rate estimation requests by not processing requests without street address. That would also be just a bandaid to an open wound - and still fully usable for bots: just add a random street address to the requests.
I think Shopify should make changes to the Cart API - at least the rate prepare feature - so that it can only be triggered from the backend using App blocks or similar, without limiting the possibility for themes or real customer to use this from the storefront.
Hey @Samuli-Approosters - thanks for flagging this and for the detailed write-up.
Just confirming one piece here: if the carrier service request your app receives is signed by Shopify, that confirms Shopify made the callback to your app. It doesn’t necessarily confirm that Shopify was the original actor initiating the rate calculation though. A storefront cart/rate request can trigger Shopify to calculate shipping rates, which can then result in Shopify calling the carrier service callback URL.
So based on what you described, it does sound plausible that anonymous storefront traffic or an external script is triggering legitimate Shopify-originated carrier service callbacks. I definitely understand why that’s not ideal from an app/provider side, especially when the address details are minimal and the product/country/zip patterns look scripted.
Could you share a bit more detail so we can better understand the pattern here?
- Are these requests tied to one shop, or are you seeing this across multiple shops using your app?
- Roughly what kind of request volume/spike are you seeing, and over what time window?
- Do the requests consistently use the same cart/products with only country/zip changing?
- If you have a couple of recent example timestamps, affected shop domains, and any Shopify request IDs from the callback headers, that would be super helpful too. Just don’t share any secrets or credentials publicly.
I’m happy to pass this along on our end as well, since I get the concern around public cart rate estimation being used to generate load against carrier service apps.
Hi @Alan_G and thanks for chiming in!
Normally our app receives around 40 000 - 60 000 carrier service requests per day. It’s quite steady. When the bots or scripts start requesting rates through the Cart API (we can see that it’s not from real checkouts), we receive up to 500 000 requests within hours.
The massive amount of requests are triggered from several stores at the same time (maybe 10-15 different stores). Usually these are Shopify Plus stores. We haven’t seen any other connection between themes or apps used in these stores. There are no shipping rate calculations in these stores for visitors to calculate rates before checkout.
The requests usually contain the same product, only the zip/country codes are changing.
Some recent dates when this has happened: April 13th, May 4th, May 5th. There are no connection in time of the day - sometimes it’s night, sometimes morning, sometimes afternoon.
I’ll try to get more details for you tomorrow.
@Alan_G
Some more details of the latest incident when we had started logging the requests:
The incident started 5th of May at 11:03 CET. The request spike was caused by more stores than I estimated: around 20-25. We didn’t log the request ID’s but we’ll add it now.
Store ID’s for some of the stores causing this (high amount of requests in short time without address details):
25380192302 (Plus)
94732419415 (Plus)
22937935 (Plus)
99513729369 (Grow)
67665953082 (Basic)
1892253766 (Grow)
54115270826 (Plus)
Are you able to see something about this from your end? This 5th of May spike wasn’t as bad as it was at 4th of May and 13th of April: those dates the request amounts were almost double compared to this.
Here’s a screenshot where you can see the spike:
Hey @Samuli-Approosters - thanks for sending these examples over, really appreciated
I took a look on our end and the examples you shared do line up with some activity I’m seeing in our logs. I can’t say for certain what’s happening, but the pattern does look consistent with the concern you raised here.
I’m going to pass this along internally with the shop examples and timing. If you can add logging for the Shopify request IDs on the carrier service callback side, that would still be the most useful next data point for tracing specific callback attempts. A few examples with UTC timestamps, shop domain, and request ID would be enough.
For mitigations on your app side, I’d still treat caching/throttling/anomaly detection by shop + product + destination pattern as useful short-term safeguards, but I get that those are workarounds and not a complete fix for public cart-rate generation. Let me know if you need help with those and I can offer some suggestions for sure.
I’ll loop back once I have more to share!