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

Order validation flowchart

Validation runs when the shopper confirms the payment, once for every item in the cart:

  1. We resolve the item's data-item-url attribute into an absolute URL.
  2. We check that this URL is on one of your allowed domains.
  3. We send a GET request to it.
  4. If the response Content-Type is application/json, our JSON crawler reads it. Otherwise we parse the HTML and look for the buy button carrying the item's id.
  5. 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.

Was this article helpful?