On this page
If your Shopify store drops limited inventory (sneakers, hype apparel, tickets, console restocks), plan for automated buyers on every drop. This guide is about what Maskbreak can and cannot do about them on Shopify, and the setup that works today.
Some drop bots run full browser automation, often through residential proxies, and rotate fake-browser profiles between attempts. Those are the signals Maskbreak reads: the visitor's own connection (VPN, proxy, Tor) and the browser itself (automation, emulators, tampered or fake browsers). Others never open a page of your store and call Shopify's cart and checkout directly, which no script on your storefront can see. Where Maskbreak can read anything on Shopify is the part that needs care.
What is possible on Shopify, and what is not
Shopify runs your cart and checkout. That sets the limits, on every plan:
- Maskbreak's script runs on your storefront pages (product, collection, cart), because you control the theme.
- It does not run inside Shopify's checkout. Third-party scripts cannot add evidence to Shopify's checkout pages, and
checkout.liquidis no longer an option. - Maskbreak cannot refuse a cart or an order by itself. Shopify's cart and checkout answer without asking your server, so a bot that calls them directly never meets a check of yours.
- Every check needs a server of yours. The API key is a secret, so the call to Maskbreak comes from a backend you run: a custom app, or a small server that receives Shopify's order webhook. A theme alone cannot call Maskbreak.
What that leaves is a real, useful setup: check each shopper who reaches checkout from your cart, attach the evidence to the order, evaluate it from your backend when the order arrives, and hold suspicious orders before you fulfil them. That stops the goods from leaving, not the order from being placed. Shopify's own bot protections stay on; this does not replace them.
Step 1: Add the script to the page with your form (the cart)
In your theme (Online Store, Themes, Edit code), open layout/theme.liquid and add this just before </body>. The first line loads the script. The second part runs when a shopper presses Checkout on a cart form: it asks the script for the evidence with Sentinel.collect(), writes it into three cart attributes, and then lets the checkout continue. Shopify copies cart attributes onto the order.
{%- comment -%} layout/theme.liquid, just before </body> {%- endcomment -%}
<script async src="https://maskbreak.com/assets/sentinel.js"></script>
<script>
document.addEventListener('submit', function (e) {
var form = e.target;
var button = e.submitter;
// Only the cart form's Checkout button, once, and only where the script loaded.
if (form.dataset.maskbreak || !window.Sentinel || !form.requestSubmit) return;
if (!button || button.name !== 'checkout') return;
if (!/\/cart\/?$/.test(new URL(form.action, location.href).pathname)) return;
e.preventDefault();
form.dataset.maskbreak = '1';
window.Sentinel.collect().catch(function () { return {}; }).then(function (ev) {
var put = function (name, value) {
var input = form.querySelector('input[name="' + name + '"]');
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = name;
form.appendChild(input);
}
input.value = value || '';
};
put('attributes[maskbreak_token]', ev.token); // the network token
put('attributes[maskbreak_fp]', ev.fingerprintEventId); // the browser check
put('attributes[maskbreak_tz]', ev.tz);
// On to checkout, one task later: a collect() that answers at once
// lands while the browser is still inside this submit, and a second
// submit started there is silently dropped.
setTimeout(function () { form.requestSubmit(button); }, 0);
});
}, true);
</script>
collect() waits at most five seconds for the browser check, then the checkout continues with whatever evidence there is; a shopper whose blocker stopped the script goes straight through. Accelerated checkout buttons (Shop Pay, Apple Pay and the like), a cart drawer whose Checkout button is not a submit button named checkout, and anything that calls Shopify's cart directly skip this step, so those orders arrive without the attributes. Your theme may need the selector adjusted; check that a test order carries the three attributes before relying on it.
Wire it into your own app: a free key returns decision, risk_score and reasons for every visit, 10,000 checks a month on the Free plan, no card.
Get an API keyStep 2: Send the check from your server
Subscribe a small backend to the orders/create webhook: from a custom app, or under Settings, Notifications, Webhooks in the Shopify admin, which also shows the secret the webhooks are signed with. The handler verifies the signature, answers Shopify at once, then sends the check. It sends whatever evidence the order carries, including none: a check without a token or a browser check id is how the console tells you which half is missing. In watch mode, the default, it only logs.
// server.js: Node 18+ with Express
const crypto = require('crypto');
const express = require('express');
const MODE = process.env.MASKBREAK_MODE || 'watch'; // 'watch' or 'enforce'
const seen = new Set(); // webhook ids already handled; use your database in production
const app = express();
app.post('/webhooks/orders-create', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
const given = req.get('X-Shopify-Hmac-Sha256') || '';
const expected = crypto.createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
.update(req.body).digest('base64');
if (given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
res.sendStatus(200); // answer Shopify first; the check runs after
const id = req.get('X-Shopify-Webhook-Id');
if (id && seen.has(id)) return; // Shopify can deliver a webhook twice
if (id) seen.add(id);
checkOrder(JSON.parse(req.body.toString('utf8')))
.catch(err => console.warn('[maskbreak] order check failed:', err.name));
});
async function checkOrder(order) {
const attrs = Object.fromEntries((order.note_attributes || []).map(a => [a.name, a.value]));
let result = null;
try {
const res = await fetch('https://maskbreak.com/v1/evaluate', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.MASKBREAK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: attrs.maskbreak_token || '',
fingerprintEventId: attrs.maskbreak_fp || undefined,
tz: attrs.maskbreak_tz || undefined,
email: order.email || undefined,
accountId: order.customer ? String(order.customer.id) : undefined,
}),
signal: AbortSignal.timeout(3000),
});
if (res.ok) result = await res.json();
else console.warn('[maskbreak] HTTP', res.status); // 401: the key, 400: the token
} catch (err) {
console.warn('[maskbreak] unavailable:', err.name);
}
// One line per order. Never log the key, the token or customer data.
console.log('[maskbreak]', JSON.stringify({
mode: MODE,
order: order.id,
decision: result ? result.decision : null,
reasons: result ? result.reasons : null,
device_evidence: result ? result.device_evidence : null,
degraded: result ? !!result.degraded : null,
}));
if (MODE === 'enforce') {
// review or block: hold it. No network token, or no answer at all: the
// order was never checked (an accelerated checkout button, a blocked
// script, or a bot that skipped your cart), which is not a verdict.
const tag = result && result.decision !== 'allow' ? 'maskbreak-' + result.decision
: !result || result.degraded ? 'maskbreak-unchecked' : null;
if (tag) await tagOrder(order.admin_graphql_api_id, tag);
}
}
// Enforce mode: tag the order (maskbreak-review, maskbreak-block or
// maskbreak-unchecked) for the people who fulfil it. Needs an Admin API token
// with the write_orders scope.
async function tagOrder(orderId, tag) {
const res = await fetch(`https://${process.env.SHOPIFY_SHOP}.myshopify.com/admin/api/2026-07/graphql.json`, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'mutation ($id: ID!, $tags: [String!]!) { tagsAdd(id: $id, tags: $tags) { userErrors { message } } }',
variables: { id: orderId, tags: [tag] },
}),
signal: AbortSignal.timeout(5000),
});
// A missing scope or a bad id still answers 200, with the reason in the body.
const out = res.ok ? await res.json() : null;
if (!out || out.errors || (out.data && out.data.tagsAdd && out.data.tagsAdd.userErrors.length)) {
console.warn('[shopify] tagging failed:', res.status, JSON.stringify(out && (out.errors || out.data)));
}
}
app.listen(process.env.PORT || 3000);
Set MASKBREAK_API_KEY (your live key), SHOPIFY_WEBHOOK_SECRET and, for enforce mode, SHOPIFY_SHOP and SHOPIFY_ADMIN_TOKEN in the server's environment. None of them goes in the theme: anything in theme code is readable by every visitor. Use an Admin API version that Shopify still supports in the URL.
Step 3: Submit your form once (place one order)
Put a product in the cart in an ordinary browser, wait a moment, press Checkout and complete one test order (with Shopify's test payment mode, or a discount code that makes it free). The first check then appears in the Maskbreak console (if half of it did not arrive, the console says which half), and your server logs one [maskbreak] line for the order. degraded: true means the order carried no network token: it skipped the cart form, or the script was blocked. device_evidence: "not_sent" means no browser check id arrived; "ok" with degraded: false is a complete check. The answer's evidence block says how old each reading was by the time the order arrived.
Holding orders in enforce mode
After a few days of comparing the log lines with the console's Events, set MASKBREAK_MODE=enforce. The handler then tags every order Maskbreak answered with review or block, and the people who fulfil orders hold those until someone has looked: an order that has not shipped can still be cancelled and refunded. For limited drops, that is the difference that matters: the stock stays on your shelf for the next real customer.
It also tags maskbreak-unchecked on every order that arrived with no network token or got no answer. That is not a verdict: a blocked script and a bot that skipped your cart land there, and so does every real customer who used an accelerated checkout button. Outside a drop, leave those alone. During a drop, look at the unchecked orders for the limited products too, because a bot that calls Shopify's cart directly only ever shows up there.
Route on decision, never on a single signal. A plain commercial VPN comes back as review, not block: plenty of real customers use one out of privacy habit, and a VPN alone is a weak signal. A proxy, automation, an emulator or a fake or tampered browser is what turns an answer into block.
Getting started
The Free plan at maskbreak.com/signup includes 10,000 visitor checks a month, up to 1,000 an hour, enough to pilot on your next drop. Paid plans raise the allowance, from €29 a month for 50,000 checks (up to 5,000 an hour); larger volumes are on Enterprise.
2026 update: shopping agents are not sneaker bots
The most significant change for merchants since this was first published is that some automated checkout traffic is a customer's assistant acting on their instruction. It arrives headless, it often egresses from cloud infrastructure, and it looks identical to inventory-hoarding automation under any rule that asks only "is this a bot?"
Blocking it is a revenue decision, and usually the wrong one. The pattern that works is to stop deciding at the edge of the store and start deciding at the edge of the action: browsing and search stay open, and the scrutiny goes on the orders for limited-release inventory and discount codes, where the loss actually concentrates. Telling assistants from attackers covers verifying declared agents against published ranges, and the decision guide maps each action to a response.
Questions people ask
- Does Maskbreak work with Shopify's built-in Bot Protection?
- They are separate layers. Shopify's protections run inside Shopify's storefront and checkout, and you should keep them on. Maskbreak runs where you add it: a script on your storefront pages and a check sent from your own backend when an order arrives. It does not replace Shopify's protections and cannot change them.
- Do I need Shopify Plus to use Maskbreak?
- No. The setup in this guide works on every plan: theme code, cart attributes and an orders/create webhook, plus a backend of your own (a custom app or a small server) that holds the API key and calls Maskbreak. On no plan does Maskbreak's script run inside Shopify's checkout; a gate before the order would need a checkout extension or validation design of your own, and Shopify restricts what those may call.
- Can Maskbreak block a bot at Shopify's checkout?
- Not with this setup. Shopify's cart and checkout answer without asking your server, so a bot that calls them directly never meets your check. What you can do is check each shopper who reaches checkout from your cart, evaluate the evidence when the order arrives, hold review and block orders before fulfilment, which keeps the goods from leaving, and look at the orders that arrived without any evidence.
- Will this slow down my checkout?
- Only by the browser check: when a shopper presses Checkout, the cart waits for the evidence, at most five seconds, before moving on. The call to Maskbreak happens later, from your server, when Shopify sends the order webhook, so it adds nothing to the shopper's checkout.
- Can I use this for a single drop and then turn it off?
- Yes. There's no commitment. Sign up free, deploy before your drop, and move to a monthly paid plan for the drop month if you need more volume, then cancel afterwards (the plan runs to the end of the paid period).
Paste it in, then watch the verdicts
The public sk_test_sandbox key returns the documented allow, review and block shapes with no account, so the failure path is testable before you go live. SDKs for Node, Python and PHP, or plain HTTP. The API reference and the Free plan cover the rest.



