Order validation
Creating Snipcart products boils down to adding a button to your site and defining it with data attributes.
You may wonder:
"What if I change product information with my browser developer tools? I'll be able to tamper with the price and place fraudulent orders."
Our order validation process is how we guarantee order integrity through the whole checkout. Before an order is created, our servers fetch every item of the cart from your own website and compare what the shopper is about to pay with what your site actually advertises.
- How it works
- What we compare
- The data-item-url attribute
- Allowed domains
- The requests we send
- JSON crawler
- Fetching products from a JSON document
- Troubleshooting
How it works
Validation runs when the shopper confirms the payment, once for every item in the cart:
- We resolve the item's
data-item-urlattribute into an absolute URL. - We check that this URL is on one of your allowed domains.
- We send a
GETrequest to it. - If the response
Content-Typeisapplication/json, our JSON crawler reads it. Otherwise we parse the HTML and look for the buy button carrying the item's id. - We compare the definition we crawled with the item sitting in the cart.
Every item has to pass. If a single one fails, the payment is refused and no order is created.
What we compare
| What | HTML crawler | JSON crawler |
|---|---|---|
| Item id | data-item-id |
id |
| Price | data-item-price |
price |
| Alternate prices | data-{discount attribute} |
otherPossiblePrices |
| Custom fields, and the price modifiers on their options | data-item-custom{n}-* |
customFields |
| Dimensions | data-item-weight, data-item-width, data-item-height, data-item-length |
dimensions |
| Digital good file | data-item-file-guid |
fileGuid |
| Subscription plans | data-plan{n}-* |
availablePlans |
| Maximum quantity | data-item-max-quantity |
— |
| Stock | — | stock, allowOutOfStockPurchases |
The item's name, description and image are not compared — they can't change what the shopper pays.
The data-item-url attribute
When configuring a product, there's a required attribute called data-item-url. It's where we go looking for the item's definition, so it must be a URL where the Snipcart buy button for that product is available.
Four forms are accepted:
| Form | Example | We request |
|---|---|---|
| Absolute | https://example.com/products/1 |
the URL as-is |
| Root-relative | /products/1 |
your default domain, plus the path |
| Protocol-relative | //shop.example.com/products/1 |
the given host, with your configured protocol |
| Query string only | ?product=1 |
your default domain, plus the query string |
The three relative forms are resolved against the default website domain set in your dashboard, using the protocol configured alongside it. They can't be used at all while that field is empty — the order fails with a message asking you to fill it in.
Important notice: for users with a single-page website, the data-item-url field should only be filled with your root domain name, such as www.example.com, or with a simple slash bar /.
A fragment (#details) is stripped from the URL before we send the request.
You can read more about this in the security entry.
Allowed domains
Before you start selling, you need to allow domains and sub-domains where Snipcart can crawl your products. In your dashboard, under Store configurations → Domains & URLs, you can set your default domain name as well as the additional allowed domains and sub-domains.
We accept a crawled URL when its host matches your default domain or one of your allowed domains. Matching is on the host name, and a www. prefix is ignored on both sides — example.com and www.example.com are interchangeable.
Anything else has to be listed explicitly. For instance, if the data-item-url value is https://test.example.com/products/1 and your default domain is example.com, our validation will fail. You need to add test.example.com in the allowed sub-domains for it to work.
The requests we send
Every crawl is a plain GET from our servers, with no cookies and no authentication, so the URL has to be publicly reachable. If you're building locally, this post shows how to develop a Snipcart-powered website behind ngrok.
Three headers identify a request as ours:
User-Agent: Snipcart/1.0
X-Snipcart-Purpose: Crawling
X-Snipcart-RequestToken: {token}We also send Cache-Control: no-store, max-age=0, and we follow a single redirect hop — 301, 302, 307 and 308.
Validating the request token
X-Snipcart-RequestToken is a random token generated by our servers. To confirm that a request really came from Snipcart, call our API with it, authenticated with your secret API key:
GET https://app.snipcart.com/api/requestvalidation/{token}A 200 means the token is genuine. The response body also carries the resource we asked for, so you can check it against the URL that was hit. A 404 means the token is unknown, already validated, or expired.
Two constraints are specific to crawling requests:
- The token is valid for one minute. Validate it while you're handling the request, not from a queue or a background job.
- The token can be validated once. A second call returns
404.
The same header is sent with webhook requests.
JSON crawler
When Snipcart validates an order's integrity, it uses the value specified in each product's data-item-url attribute.
Most times, the value specified for this attribute will be the unique URL where you're selling the item, and our HTML crawler finds the buy button on that page. However, merchant sites can sometimes require more complex validation scenarios.
If that's your case, there's an alternative to our default HTML crawler: our JSON crawler. There is nothing to turn on — when Snipcart makes the request to the URL, if your response Content-Type header is application/json, we'll use our JSON validator instead of the HTML one.
You must return us a JSON object with the following properties.
| Property | Required | Description |
|---|---|---|
id |
Yes | The same id you specified in the product definition in your HTML. |
price |
Yes | A number, or an object keyed by currency for multi-currency stores. |
customFields |
When the item has custom fields | It only needs to contain the fields that are required or that change the price. Return [] when none of them do. |
url |
Recommended | Not compared during validation, but used when fetching products into your dashboard. |
dimensions |
When the item has measurements | weight, width, height and length. |
fileGuid |
For digital goods | Must match the item's file GUID. |
otherPossiblePrices |
For alternate-price discounts | An array of code / price objects. |
availablePlans |
For subscriptions | The plans the item can be bought with. |
stock, allowOutOfStockPurchases |
Optional | Checked at checkout, not only when fetching products. |
valid |
Optional | Accepts the item without any other check. |
{
"id": "20",
"price": 50.00,
"customFields": [],
"url": "https://example.com/products/1.json"
}Custom fields
Only the custom fields that are required or that change the price have to be listed. If the item has custom fields but none of them do, return an empty array.
{
"id": "20",
"price": 50.00,
"customFields": [
{
"name": "Frame color",
"options": "Black|Brown[+100.00]|Gold[+300.00]"
},
{
"name": "Note",
"required": true
}
],
"url": "/"
}Options and their price modifiers must be the same ones as in your product definition. A field carrying a price modifier that is missing from your response fails validation as a price mismatch.
Multiple currencies
If you are using our multi-currency feature, the price property can be an object with multiple currencies.
{
"id": "20",
"price": {
"usd": 30.00,
"cad": 35.00
},
"url": "/"
}Every currency you sell in has to be there. If the cart's currency is missing from the object we can't read a price at all, and the item is reported as unreachable rather than as a price mismatch.
Dimensions
If you intend to provide any specific measurements for your product, include the dimensions property. It is compared as a whole, so sending dimensions for an item that doesn't declare any fails the same way as sending the wrong ones.
{
"id": "20",
"price": 50.00,
"dimensions": {
"weight": 300,
"width": 20,
"height": 10,
"length": 30
},
"url": "/"
}Alternate prices
If a discount in the cart sets an alternate price, list it under otherPossiblePrices. The code is the alternate price name configured on the discount — the same name you would put in the data- attribute with the HTML crawler.
{
"id": "20",
"price": 50.00,
"otherPossiblePrices": [
{
"code": "vip",
"price": 35.00
}
],
"url": "/"
}Without it, a cart holding that discount fails with a price mismatch.
Returning several products at once
You can also return an array containing multiple objects as defined above. We look for the entry whose id matches the item being validated.
[
{
"id": "20",
"price": 50.00,
"url": "https://example.com/products.json"
},
{
"id": "21",
"price": 100.00,
"url": "https://example.com/products.json"
}
]This can be useful when your website is API-driven, using a single page application framework such as React or Vue.
Skipping the checks for an item
Returning "valid": true tells us your own endpoint has already verified the line item. We accept it as-is and skip every other check — price, custom fields, dimensions, file GUID, plans and stock included.
{
"valid": true
}Use it sparingly. Because it short-circuits everything, an endpoint that answers valid: true to every request — a catch-all route, for instance — turns price validation off for your whole store, and nothing in your dashboard will tell you.
Inside an array, the entry is still located by its id first.
Fetching products from a JSON document
From our dashboard, you can fetch products if you need to set inventory stock, for instance. If you are using our JSON validator you can also use your JSON documents to fetch products.
The document can be a JSON file containing all of your products in an array or a product individually.
A fetch reads more properties than validation does: name, image, description, categories, metadata, inventoryManagementMethod and variants.
The following example is a single product that can be fetched. Please note that this example also sets default stock levels.
{
"id": "JSON_PRODUCT",
"name": "JSON Product",
"url": "/products.json",
"price": 20.00,
"image": "http://placehold.it/300x300",
"inventoryManagementMethod": "Variant",
"dimensions": {
"weight": 300,
"width": 20,
"height": 10,
"length": 30
},
"variants": [
{
"variation": [
{
"name": "Color",
"option": "Red"
},
{
"name": "Size",
"option": "Small"
}
],
"stock": 10,
"allowOutOfStockPurchases": true
}
],
"categories": ["category1", "category2"],
"customFields": [
{
"name": "Size",
"options": "Small|Medium|Large",
"type": "dropdown"
},
{
"name": "Color",
"options": "Red|Blue|Green",
"type": "dropdown"
}
]
}Troubleshooting
| What you see | Where to look first |
|---|---|
| We couldn't reach the item | The URL isn't publicly reachable, its domain isn't among your allowed domains, or data-item-url is relative while your default domain is empty. |
| The item wasn't found | The id on the crawled page, or in the JSON document, doesn't match the item's data-item-id. |
| The price doesn't match | The price changed after the item was added to the cart, an alternate-price discount is missing from otherPossiblePrices, or a custom field price modifier is missing from the crawled definition. |
| A required custom field is missing | The crawled definition marks a field as required and the cart has it empty. |
Items are validated against the price, custom fields and options they had when they were added to the cart. Changing item attributes from JavaScript once the item is in the cart causes a mismatch.