API reference

Last updated

API reference#

POST products

This returns a list of products. You send a JSON body to search and filter the results. Described below.

GET products/32245

This returns a single product.

POST uri

This returns a list of products in a category, or single product, depending on what the URI leads to.

GET categories

Returns a list of categories.

Products Endpoint#

If you use POST products without a JSON body, you get all products back. You use the JSON body to filter and search the products, so you get fewer back.

The response object contains these:

1 2 3 4 5 6 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "products": [...], "productCount": 344, "filter": [...] }
  • token: the session token that you always have in this API
  • products: an array of products
  • productCount: the total number of products without paging. So you can show "page 1 of 7" even if you only fetch 50 at a time.
  • filter: is the filter values of the products you are viewing now, also without paging. So you know there are 35 red ones and 12 blue for example.

The request you send can contain these fields, everything is optional:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "skipFirst": 5, "limit": 10, "categories": [1,2,3], "collections": [1,2,3], "silkProduct": 123, "search": "hello world", "products": [1,2,3], "relatedProducts": true, "brands": [1,2,3], "swatch.desc": ["Red", "Blue", "Green"], "items.name":["W24\/L30"], "onlyAvailable":true, "uri":{ "uri":"jeans\/black", "for":["product", "category"] }, "sortOrder": [ { "field": "priceAsNumber", "order": "desc" } ] }
  • skipFirst + limit: for paging
  • categories: you want products in these categories
  • search: free text search
  • relatedProducts: when a product has relatedProducts and this is true, you get the complete data for those releated products. Otherwise you will get a small subset of the data back: only the media and product id.
  • swatch.desc: filtering based on the color swatch (This is a client specific field, and not all Centra instances will have this field)
  • items.name: filtering on specific item names
  • onlyAvailable: true means you only get back products that are in stock or available for preorder. If you also specify items.name, those items must be available
  • uri: filter on a product or category with a specific URI
  • sortOrder: Sort returned products based on the specified field, ascending or descending. Currently you can filter on: uri, categoryItemSort, collectionUri, priceAsNumber, createdAt and modifiedAt, in either asc or desc order

Remember that there is a special case for related products: If a product relation is a variant, it will appear regardless of the relatedProducts parameter. However, if the relation is standard, it will only appear if relatedProducts is set to true.

If you select more "Product Filter Fields" in the Centra plugin settings, you can send them in the request and also get them back in the response in the "filter".

Example#

1 2 3 4 5 6 7 8 POST products?pretty { "limit": 2, "skipFirst": 5, "search": "som", "categories": [709], "swatch.desc": ["Red","Blue"] }

(notice ?pretty in the URL, this will return indented JSON)

This means products must match:

Free text search for "som" AND category = 709 AND swatch.desc = (Red OR Blue)

Paging is return 2 products, and skip the first 5.

So how do you know about category 709? Or that swatch.desc can be Red or Blue? This is what the "filter" in the response is for. It contains all possible filtering values, and a count of how many products matches each filtering value in the current set of filtered products and for all products.

Response:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "products": [ { "product": "22068", "variant": "30372", "name": "Avery solid mohair beanie ice blue", "uri": "avery-solid-mohair-beanie-ice-blue", "sku": "SKU1SKU1", "productSku": "SKU1SKU1", "brand": "1", "brandName": "Some Brand", "brandUri": "some-brand", "collection": "51", "collectionName": "Last Season", "collectionUri": "last-season", "variantName": "smoke", "countryOrigin": "CN", "excerpt": "", "excerptHtml": "", "description": "Oversized beanie in a mohair blend", "descriptionHtml": "<p>Oversized beanie in a mohair blend.", "metaTitle": "Solid Mohair beanie | Some Brand ", "metaDescription": "An oversized beanie in a mohair blend.", "metaKeywords": "Women\\'s knitted beanie hat", "stockUnit": "", "category": "709", "categoryName": [ "Women\\'s", "Sale" ], "categoryUri": "womens\/sale", "centraProduct": "26453", "centraVariant": "22068", "itemQuantityMinimum": 1, "itemQuantityMultipleOf": 1, "price": "150\u00a0SEK", "priceAsNumber": 150, "discountPercent": 70, "priceBeforeDiscount": "500\u00a0SEK", "priceBeforeDiscountAsNumber": 500, "showAsOnSale": true, "itemTable": { "x": [ "onesize" ], "y": [ "" ], "dividerSymbol": "x" }, "items": [ { "item": "22208-126748", "itemTableX": 0, "itemTableY": 0, "name": "onesize", "ean": "123123123", "sku": "SKU1SKU1" } ], "media": { "standard": [ "https:\/\/example.net\/client\/dynamic\/images\/26453_c80b4e751c-5.jpg" ] }, "swatch": { "desc": "Blue", "hex": "Blue" }, "categories": { "709": { "category": "709", "name": [ "Women\\'s", "Sale" ], "uri": "womens\/sale" } }, "relatedProducts": [ ] }, { "product": "22069", "name": "Snapback baseball cap winter white", "uri": "snapback-baseball-cap-winter-white", "sku": "SKU2SKU2", "productSku": "SKU2SKU2", "brand": "1", "brandName": "Some Brand", "brandUri": "some-brand", "collection": "51", "collectionName": "Last Season", "collectionUri": "last-season", "variantName": "winter white", "countryOrigin": "CN", "excerpt": "", "excerptHtml": "", "description": "ladies\\' baseball cap\n[b]QUALITY:[\/b]\nfurpile synthetic", "descriptionHtml": "<p>ladies' baseball cap<br \/>\n<strong>QUALITY:<\/strong><br \/>\nfurpile synthetic<\/p>", "metaTitle": "Snapback baseball cap | Some Brand ", "metaDescription": "", "metaKeywords": "", "stockUnit": "", "category": "709", "categoryName": [ "Women\\'s", "Sale" ], "categoryUri": "womens\/sale", "centraProduct": "26589", "centraVariant": "22204", "itemQuantityMinimum": 1, "itemQuantityMultipleOf": 1, "price": "120\u00a0SEK", "priceAsNumber": 120, "discountPercent": 70, "priceBeforeDiscount": "400\u00a0SEK", "priceBeforeDiscountAsNumber": 400, "showAsOnSale": true, "itemTable": { "x": [ "onesize" ], "y": [ "" ], "dividerSymbol": "x" }, "items": [ { "item": "22069-127296", "itemTableX": 0, "itemTableY": 0, "name": "onesize", "ean": "234234234", "sku": "SKU2SKU2" } ], "media": { "standard": [ "https:\/\/example.net\/client\/dynamic\/images\/26589_e31b009b90-g409647021.jpg" ] }, "swatch": { "desc": "Red", "hex": "ff0000" }, "categories": { "709": { "category": "709", "name": [ "Women\\'s", "Sale" ], "uri": "womens\/sale" } }, "relatedProducts": [ ] } ], "productCount": 7, "filter": [ { "field": "brands", "values": [ { "value": "1", "count": 7, "totalCount": 344, "data": { "brand": "1", "brandName": "Some Brand" } } ] }, { "field": "categories", "values": [ { "value": "599", "count": 0, "totalCount": 49, "data": { "category": "599", "name": [ "Jeans" ], "uri": "jeans" } }, { "value": "62", "count": 0, "totalCount": 23, "data": { "category": "62", "name": [ "Jeans", "T-shirts" ], "uri": "jeans\/t-shirts", "inCategory": "599" } }, { "value": "14", "count": 0, "totalCount": 17, "data": { "category": "14", "name": [ "Jeans", "Shirts " ], "uri": "jeans\/shirts", "inCategory": "599" } }, { "value": "3", "count": 7, "totalCount": 69, "data": { "category": "3", "name": [ "Women\\'s" ], "uri": "womens" } }, { "value": "320", "count": 0, "totalCount": 3, "data": { "category": "320", "name": [ "Women\\'s", "Tops" ], "uri": "womens\/tops", "inCategory": "3" } }, { "value": "709", "count": 7, "totalCount": 38, "data": { "category": "709", "name": [ "Women\\'s", "Sale" ], "uri": "womens\/sale", "inCategory": "3" } } ] }, { "field": "collections", "values": [ { "value": "27", "count": 0, "totalCount": 37, "data": { "collection": "27", "collectionName": "Spring" } }, { "value": "51", "count": 7, "totalCount": 95, "data": { "collection": "51", "collectionName": "Last Season" } } ] }, { "field": "swatch.desc", "values": [ { "value": "Red", "count": 1, "totalCount": 35, "data": { "desc": "Red", "hex": "ff0000" } }, { "value": "Green", "count": 0, "totalCount": 14, "data": { "desc": "Green", "hex": "00ff00" } }, { "value": "Blue", "count": 6, "totalCount": 12, "data": { "desc": "Blue", "hex": "Blue" } } ] } ] }

The "filter" object has values for the field "swatch.desc" at the end of this JSON blob. And the last thing is value "Blue". "count":6 means there are 6 blue products in the current filtered set of products, "totalCount":12 means there are 12 blue products in total without filtering. The "data" object contains the data the frontend should use to display "Blue", it is the same data as the "swatch" on the product itself.

In the filter object, the only thing that changes depending on what you filter on is the "count". If you do not filter on anything count = totalCount.

Searching improvements since v2.92#

Using the free-text "search" field in POST /products endpoint now has the following improvements:

  • Handling of non-Latin characters (e.g. “grön”, “små”),
  • Matching Latin to non-Latin characters (searching “grön” will also match “gron”),
  • Proximity (fuzzy) matching of phrases (searching “prdct” should match “product”).

For example, if you have a Product named “Test Product” which contains “Grön” in its description, you should be able to find it using phrases like: "test product", "test grön", "product gron", or even "prdct gron".

Fuzzy search only works on words longer than 3 characters.

Routing#

You can post a uri to the POST products endpoint to filter out products that have a specific uri or that are in a category with a specific uri:

1 2 3 4 5 6 7 POST products?pretty { "uri": { "uri": "jeans/slim-5-pocket-jeans-white", "for": ["category", "product"] } }

You will get 1 product back for this, if a product exists with the uri "jeans/slim-5-pocket-jeans-white". The "uri" object is the URI filtering:

  • uri.uri is the uri
  • uri.for is what the uri is for. It can be "category" and/or "product".

There is a more generic endpoint for this:

POST uri

You post a uri, and what the uri can be for. Just like POST products:

1 2 3 4 5 6 7 POST uri { "uri": "jeans/slim-5-pocket-jeans-white", "for": ["category", "product"], "limit": 2, "skipFirst": 0 }
  • uri is the uri
  • for is what it is for. It can be "product" or "category".
  • limit + skipFirst is paging, used when you get an array of products in a category or back to limit the number of products

The response changes depending on what was found. The plan is that we will add new things to this endpoint in the future, maybe CMS articles. You would then be able to POST "for":["category", "cms"] and get back CMS articles or a category of products. But you have to explicitly ask for it, you will not get back unknown results.

Example: URI leads to a product#

The response has "found": "product" and a "product" object:

1 2 3 4 5 6 7 POST uri { "uri": "jeans/slim-5-pocket-jeans-white", "for": ["category", "product"], "limit": 2, "skipFirst": 0 }

Response:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "found": "product", "product": { "product": "24379", "name": "Slim 5-pocket jeans white", "uri": "slim-5-pocket-jeans-white", "sku": "", "productSku": "", "brand": "1", "brandName": "Some Brand", "brandUri": "some-brand", "collection": "55", "collectionName": "Summer", "collectionUri": "summer", "variantName": "white", "countryOrigin": "TR", "excerpt": "", "excerptHtml": "", "description": "Classic 5-pocket jeans", "descriptionHtml": "<p>Classic 5-pocket jeans<\/p>", "metaTitle": "Eddy 5-pocket jeans | Some Brand ", "metaDescription": "", "metaKeywords": "", "stockUnit": "", "category": "599", "categoryName": [ "Jeans" ], "categoryUri": "jeans", "centraProduct": "26954", "centraVariant": "22775", "itemQuantityMinimum": 1, "itemQuantityMultipleOf": 1, "price": "900\u00a0SEK", "priceAsNumber": 900, "discountPercent": 0, "priceBeforeDiscount": "900\u00a0SEK", "priceBeforeDiscountAsNumber": 900, "showAsOnSale": false, "itemTable": { "x": [ "W28L32", "W29L32", "W30L32", "W31L32", "W31L34", "W32L32", "W32L34", "W33L32", "W33L34", "W34L32", "W34L34", "W36L32", "W36L34", "W38L34" ], "y": [ "" ], "dividerSymbol": "x" }, "items": [ { "item": "24379-130563", "itemTableX": 0, "itemTableY": 0, "name": "W28L32", "ean": "", "sku": "", "stock":"yes" }, { "item": "24379-130564", "itemTableX": 1, "itemTableY": 0, "name": "W29L32", "ean": "", "sku": "", "stock":"yes" }, { "item": "24379-130565", "itemTableX": 2, "itemTableY": 0, "name": "W30L32", "ean": "", "sku": "", "stock":"yes" }, { "item": "24379-130566", "itemTableX": 3, "itemTableY": 0, "name": "W31L32", "ean": "", "sku": "", "stock":"yes" } ], "media": { "standard": [ "https:\/\/example.com\/client\/dynamic\/images\/26954_09e21e4415-banner-dress-shirts_1.jpg", "https:\/\/example.com\/client\/dynamic\/images\/26954_7e68107548-27295_821558126c-h109916999-original5.jpg", "https:\/\/example.com\/client\/dynamic\/images\/26954_a506c5a76b-h105127001_d2.jpg", "https:\/\/example.com\/client\/dynamic\/images\/26954_a7a7ad85b3-h105127001_d1.jpg", "https:\/\/example.com\/client\/dynamic\/images\/26954_ebfa438d2f-h105127001_d3.jpg" ] }, "categories": { "599": { "category": "599", "name": [ "Jeans" ], "uri": "jeans" } } } }

Example: URI leads nowhere, API returns 404#

The only difference from the previous example is the request has "for":["category"] but we know this uri leads to a product. So no category with that URI is found. The API returns status code 404, and the response has an "errors" object (the rest of the API follows this convention, if there are errors then the response contains an "errors" object).

1 2 3 4 5 6 7 POST uri { "uri": "jeans/slim-5-pocket-jeans-white", "for": ["category"], "limit": 2, "skipFirst": 0 }

Response header:

HTTP/1.1 404 Not Found

Response:

1 2 3 4 5 6 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "errors": { "uri": "not found" } }

Example: URI leads to a category, API returns a category and list of products#

The response contains "found": "category", the "category" object with the category name, and just like POST products it has "products", "productCount" and "filter". The content of these is exactly the same as POST products.

1 2 3 4 5 6 7 POST uri { "uri": "jeans", "for": ["category", "product"], "limit": 2, "skipFirst": 0 }

Response (edited to make it shorter):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "found": "category", "category": { "category": "599", "name": [ "Jeans" ], "uri": "jeans" }, "products": [ { "product": "32240", ... }, { "product": "32245", ... } ], "productCount": 49, "filter": [...] }

Categories#

GET categories returns an array of categories like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 { "token": "esf1p3tgchfg5ggtpqdpgqjtt6", "categories": [ { "category": "5", "name": [ "Some category" ], "uri": "some_category" }, { "category": "3", "name": [ "V\u00e4xt" ], "uri": "vaxt" }, { "category": "4", "name": [ "V\u00e4xt", "Buske" ], "uri": "vaxt\/buske", "inCategory": "3" }, { "category": "2", "name": [ "V\u00e4xt", "Buske", "Nypon" ], "uri": "vaxt\/buske\/nypon", "inCategory": "4" } ] }

This array is sorted in the order you set in the Centra admin. Notice that some categories in the array are under-categories of another category. You see this on last two, they have the field inCategory with the category id of the category they are a subcategory of. Also notice the name array and uri of these, they contain the full name and uri, of the main category and under-categories.

Products and categories#

A product has an array of items. An item is the thing you buy, what you add to a selection (also called cart, basket). For clothes, the items correspond to sizes of the product, and you would buy a sweater (product) in size M (item).

Products are organized into categories. Each category contains a list of products. A category can also contain categories. For example, one category could be "Clothes" and that could contain a category called "Sweaters".

A product object can contain a relatedProducts array, this array contains other product objects. These related products are related to the main product in some way, for example, it might be the same product in different colors.

Products and categories have a URI. A site normally has routes leading to specific products and categories based on the URIs. A category page should usually display the products in that category. You can ask the API what a specific URI points to, and also request all products in a specific category.

In a product listing (a category page), you would usually list only the main products and maybe indicate that a product has more colors when relatedProducts is not empty. When you view a single product, you would usually display all the related products along with it.

When you filter and search in the API, you are doing that on the main product plus all the related products. For example, if you filter on blue products, you can get back a product that is red but has a blue one in relatedProducts.

Example product#

With one related product

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 { "product": "3", "name": "Tv\u00e5", "uri": "tva", "sku": "p1v2", "productSku": "p1", "brand": "1", "brandName": "Brand", "brandUri": "brand", "collection": "1", "collectionName": "Min kollektion", "collectionUri": "minkollektion", "variantName": "V2", "countryOrigin": "", "excerpt": "Min korta display description", "excerptHtml": "<p>Min korta display description<\/p>", "description": "Min l\u00e5nga display description!\n\n\nhej\n\n\nhej\n\n\n\nhej", "descriptionHtml": "<p>Min l\u00e5nga display description!<\/p>\n<p>hej<\/p>\n<p>hej<\/p>\n<p>hej<\/p>", "metaTitle": "min display meta title!", "metaDescription": "Min display meta description!", "metaKeywords": "Min display meta keywords!", "stockUnit": "", "category": "4", "categoryName": [ "V\u00e4xt", "Buske" ], "categoryUri": "vaxt\/buske", "centraProduct": "1", "centraVariant": "2", "itemQuantityMinimum": 1, "itemQuantityMultipleOf": 1, "price": "88.89\u00a0GBP", "priceAsNumber": 88.89, "priceBeforeDiscount": "88.89\u00a0GBP", "discountPercent": 0, "showAsOnSale": false, "priceBeforeDiscountAsNumber": 88.89, "itemTable": { "x": [ "" ], "y": [ "" ], "dividerSymbol": "x" }, "items": [ { "item": "3-2", "itemTableX": 0, "itemTableY": 0, "name": "", "ean": "23143657879", "sku": "p1v2size", "stock": "yes" } ], "categories": { "4": { "category": "4", "name": [ "V\u00e4xt", "Buske" ], "uri": "vaxt\/buske" }, "3": { "category": "3", "name": [ "V\u00e4xt" ], "uri": "vaxt" } }, "available": true, "relatedProducts": [ { "product": "38", "name": "Tv\u00e5", "uri": "tva", "sku": "p1v1", "productSku": "p1", "brand": "1", "brandName": "Brand", "brandUri": "brand", "collection": "1", "collectionName": "Min kollektion", "collectionUri": "minkollektion", "variantName": "V1", "countryOrigin": "", "excerpt": "Min korta display description", "excerptHtml": "<p>Min korta display description<\/p>", "description": "Min l\u00e5nga display description!\n\n\nhej\n\n\nhej\n\n\n\nhej", "descriptionHtml": "<p>Min l\u00e5nga display description!<\/p>\n<p>hej<\/p>\n<p>hej<\/p>\n<p>hej<\/p>", "metaTitle": "min display meta title!", "metaDescription": "Min display meta description!", "metaKeywords": "Min display meta keywords!", "stockUnit": "", "category": "4", "categoryName": [ "V\u00e4xt", "Buske" ], "categoryUri": "vaxt\/buske", "centraProduct": "1", "centraVariant": "1", "itemQuantityMinimum": 1, "itemQuantityMultipleOf": 1, "price": "199.99\u00a0GBP", "priceAsNumber": 199.99, "priceBeforeDiscount": "199.99\u00a0GBP", "discountPercent": 0, "showAsOnSale": false, "priceBeforeDiscountAsNumber": 199.99, "itemTable": { "x": [ "" ], "y": [ "" ], "dividerSymbol": "x" }, "items": [ { "item": "38-1", "itemTableX": 0, "itemTableY": 0, "name": "", "ean": "asd", "sku": "p1v1", "stock": "yes" } ], "media": { "s_big": [ "https:\/\/example.com\/client\/dynamic\/images\/1_aa17ecb525-nypon_frukter-s_big.jpg", "https:\/\/example.com\/client\/dynamic\/images\/1_97b96240d4-nypon1-s_big.jpg" ] }, "categories": { "4": { "category": "4", "name": [ "V\u00e4xt", "Buske" ], "uri": "vaxt\/buske" }, "3": { "category": "3", "name": [ "V\u00e4xt" ], "uri": "vaxt" } }, "available": true } ] }

Item Table#

The product data has a itemTable with x and y arrays. This is the x and y axis of a table. And each item in the array of items has itemTableX and itemTableX integer fields.

You can use this to sort and display the items in the correct order in a table.

In the example above, the products have only a single item, so it does not illustrate how this works. This example, with jeans, has more. I have removed most data, only the itemTable and a few items are left:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 { "product": "3530", ... "itemTable": { "x": [ "W24", "W25", "W26", "W27", "W28", "W29", "W30", "W31", "W32", "W33", "W34", "W36", "W38" ], "y": [ "L30", "L32", "L34", "L36" ], "dividerSymbol": "\/" }, "items": [ { "item": "4165-32694", "itemTableX": 0, "itemTableY": 0, "name": "W24\/L30", "ean": "123123123123", "sku": "12345-L30-W24", "stock": "yes" }, { "item": "4165-32707", "itemTableX": 2, "itemTableY": 1, "name": "W26\/L32", "ean": "123123123124", "sku": "12345-L32-W26", "stock": "yes" } ] }

You can display this as a table, where 4165-32694 is at a and 4165-32707 at b

1 2 3 4 5 W24 W25 W26 W27 W28 W29 W30 W31 W32 W33 W34 W36 W38 L30 a L32 b L34 L36

Selections and orders#

A selection is an open order, or a shopping cart. You add items to the selection, the product items to buy.

If you have found an item 1313-19728 using the products endpoint, you can add it to your selection:

POST items/1313-19728 HTTP/1.1

You get back the selection. The response also has the session token. Use the token for the API-token header, like API-Token 297qi56173p6sm0l5bii30ois3

API Errors#

The API reports errors by returning response code 400 or above.

The response body will contain an "errors" object when there is an error. So there are no errors if there is no "errors".

Example:#

POST https://example.com/api/checkout/items/123-456

Response:

1 2 3 4 5 6 { "token": "0ms3rnl09a4i4brtbitt1o0cu1", "errors": { "item": "product item not found" } }

Payment flow#

1: Add something to the selection:#

POST https://example.com/api/checkout/items/1313-19728

In the response object you get back, there is

  • token: the token to use for the "API-Token" header, session cookie
  • selection: the selection (aka cart) with prices, currency
  • products: the product data
  • location: the customers country, we detect this with Geo-IP
  • paymentMethods: the payment methods you can select
  • paymentFields: the fields for the checkout.
  • countries: all countries that we ship to. includes states for example for USA

2: (optional) change country#

PUT https://example.com/api/checkout/countries/SE

You get back the same structure as in 1. This can change prices/currency and in some cases can remove items that are not for sale in the selected country.

3: (optional) select a specific payment method#

PUT https://example.com/api/checkout/payment-methods/paypal

paypal here is from the paymentMethods in the previous responses. You get back the same structure as in #1.

4: Begin payment#

POST https://example.com/api/checkout/payment

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "paymentReturnPage":"https://mywebshop.example.com/payment-return-page", "paymentFailedPage":"https://mywebshop.example.com/payment-failed-page", "termsAndConditions":true, "address":{ "firstName":"asd", "lastName":"asd", "email":"hello@example.com", "address1":"asd", "city":"asd", "country":"dk", "phoneNumber":"123123", "zipCode":"123" } }

Some integrations, like DHL shipping, require that you format the zip code (postal code) in a format that is commonly used in the shipping country. If you pass the zip code in a different format, creating a shipment can fail. It is therefore important that you follow the zip code formatting recommendation for every country you intend to ship to. For example, Swedish codes are formatted as NNN NN (with a space), in Germany you have: NNNNN, in Poland: NN-NNN, in Denmark: NNNN. A full list of postal codes formats by country can be found on Wikipedia: https://en.wikipedia.org/wiki/List_of_postal_codes. If you encounter any problems after following these guidelines, we recommend to contact DHL support.

Response:

1 2 3 4 5 6 { "token": "be37e53c31dc3d0e66933560e187ef72", "action": "redirect", "url": "https://www.sandbox.paypal.com/checkoutnow?token=20E81578H46162301", "orderId": "20E81578H46162301" }

This means you should redirect the visitor to the URL, to the payment page of paypal. The paymentReturnPage and paymentFailedPage in the request is where the visitor will return after the payment at paypal. These must on your frontend.

When the customer ends up on paymentFailedPage, you know that payment failed.

When the customer ends up on paymentReturnPage, you MUST ask the API if the payment was a success. It can still fail. You do this by forwarding the GET and POST variables that the visitor had when it accessed the paymentReturnPage to the API:

5: Get the result of the payment#

POST https://example.com/api/checkout/payment-result

1 2 3 4 5 6 7 { "paymentMethodFields": { "orderNum": "1114", "paymentMethod": "paypal", "orderRef": "ad0eccd6a1e9402facf09f6ac49e848f" } }

You take the GET and POST variables that visitor had when it returned to the "paymentReturnPage" and send them to the API inside "paymentMethodFields".

Be mindful to keep the original formatting of the parameters you receive from payment provider and pass on to Centra. Depending on the payment method they may be written in camelCase (like orderRef in PayPal) or in snake_case (like klarna_order in Klarna). Sending wrong parameter names to Centra may cause problems with receiving order confirmation and prevent you from displaying a proper receipt.

Response (cut down):

1 2 3 4 5 6 7 HTTP/1.1 200 OK { "token": "dacdi99cb9q3vv5gl5lac6gmj6", "order": "1114", "status": "untouched", "..." }

This response is a success. But i will change the format of this to match the other responses better.

Payment flow, with klarna checkout#

Similar to the above, with the following changes:

3: select klarna checkout as payment method#

PUT https://example.com/api/checkout/payment-methods/klarna-checkout

4: Begin payment#

POST https://example.com/api/checkout/payment

1 2 3 4 5 6 7 8 { "paymentReturnPage":"https://example.com/payment-return-page", "paymentFailedPage":"https://example.com/payment-failed-page", "termsAndConditions":true, "address":{ "country":"se" } }

Response:

1 2 3 4 5 { "token":"0ms3rnl09a4i4brtbitt1o0cu1", "action":"form", "formHtml":"<div id=\"klarna-checkout-container\" style=\"overflow-x: hidden;\">\n <script type=\"text\/javascript\">\n \/* <![CDATA[ *\/\n (function(w,k,i,d,n,c,l,p){\n w[k]=w[k]||function(){(w[k].q=w[k].q||[]).push(arguments)};\n w[k].config={\n container:w.document.getElementById(i),\n ORDER_URL:'https:\/\/checkout.testdrive.klarna.com\/checkout\/orders\/FZKBVVGD7PIIFMOOOE5N61Y5TKY',\n AUTH_HEADER:'KlarnaCheckout MsmS7sUBsXVCIzo80FlZ',\n LAYOUT:'desktop',\n LOCALE:'sv-se',\n ORDER_STATUS:'checkout_incomplete',\n MERCHANT_TAC_URI:'http:\/\/example.com\/terms.html',\n MERCHANT_TAC_TITLE:'Young Skilled',\n MERCHANT_NAME:'Young Skilled',\n MERCHANT_COLOR:'',\n GUI_OPTIONS:[],\n ALLOW_SEPARATE_SHIPPING_ADDRESS:\n false,\n PURCHASE_COUNTRY:'swe',\n PURCHASE_CURRENCY:'sek',\n NATIONAL_IDENTIFICATION_NUMBER_MANDATORY:\n false,\n ANALYTICS:'UA-36053137-1',\n TESTDRIVE:true,\n PHONE_MANDATORY:true,\n PACKSTATION_ENABLED:false,\n BOOTSTRAP_SRC:'https:\/\/checkout.testdrive.klarna.com\/170312-6cde26c\/checkout.bootstrap.js',\n PREFILLED: false\n };\n n=d.createElement('script');\n c=d.getElementById(i);\n n.async=!0;\n n.src=w[k].config.BOOTSTRAP_SRC;\n c.appendChild(n);\n try{\n p = w[k].config.BOOTSTRAP_SRC.split('\/');\n p = p.slice(0, p.length - 1);\n l = p.join('\/') +\n '\/api\/_t\/v1\/snippet\/load?order_url=' +\n w.encodeURIComponent(w[k].config.ORDER_URL) + '&order_status=' +\n w.encodeURIComponent(w[k].config.ORDER_STATUS) + '&timestamp=' +\n (new Date).getTime();\n ((w.Image && (new w.Image))||(d.createElement&&d.createElement('img'))||{}).src=l;\n }catch(e){}\n })(this,'_klarnaCheckout','klarna-checkout-container',document);\n \/* ]]> *\/\n <\/script>\n <noscript>\n Please <a href=\"http:\/\/enable-javascript.com\">enable JavaScript<\/a>.\n <\/noscript>\n<\/div>\n" }

This response has a different action called form, and a formHtml. You need to display this formHtml in the frontend. This thing from klarna should redirect the visitor to the paymentReturnPage or paymentFailedPage after payment. Klarna checkout gives us the customers address after the payment is done, so you only need to send the country to the API. We need the country to calculate prices correctly.

5: Get the result of the payment#

This should be exactly like for PayPal. You will get different data from klarna, but just pass it on to the API and it will tell you if the payment was successful

Payment response cases:#

POST https://example.com/api/checkout/payment can respond with an error, or one of 4 different actions:

  • "action":"redirect" like PayPal above, redirect the client to the url in the response.
  • "action":"form" like klarna-checkout above, display this htmlForm HTML-snippet. The HTML you get for klarna checkout i unusual, for other payment methods that have this response it is a HTML form that you need to POST in the visitors browser. This requires a server side page on the frontend since you cannot POST a form like this with Javascript (as far as i know).
  • "action":"success" this means the payment succeeded at once (no need for step 5 from above). It happens with some invoice payments or when the credit card number is integrated in our checkout page. The response contains the order data that you would otherwise get from step 5.
  • "action":"failed" this means the payment failed at once.

So far, these 4 cases have covered all payment integrations we have. If we implement these, the checkout should work with lots of payment providers. Klarna checkout is the most odd one, that probably needs specific treatment in the frontend.

Out of stock errors#

The API does a stock check at POST https://example.com/api/checkout/payment

If something is unavailable, you get back an error in errors, response code 410, and:

  • unavailable contains a list of the items that were unavailable. These have been removed from the selection.
  • the rest of the response looks like GET selection

Example, the selection.items has item=4-5, but quantity=5. the unavailable response looks like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 HTTP/1.1 410 Gone Content-type: application/json { "token": "atqsqi9m7llsp35hv5689m6tu5", "unavailable": [ { "item": "4-5", "product": "4", "originalQuantity": 11, "unavailable": 6, "available": 5 } ], "selection": { "currency": "SEK", "paymentMethod": "adyen-256", "..."

Selection lines bulk update#

Checkout API selection bulk update allows efficiently make multiple changes to a selection in a single request.

Features list:

  • Decreasing item quantity: reduce the quantity of an existing item in the selection.
  • Removing item from selection: remove an item from the selection by setting its quantity to 0.
  • Adding item quantity: increase the quantity of an existing item in the selection.
  • Adding item to selection: you can also add new items to the selection with a specified quantity.

By consolidating these functionalities into a single request, the API endpoint fosters efficient and user-friendly management of items in a selection, accommodating a wide range of use cases and requirements.

The new API endpoint built for that purpose is PUT /items.

Request body#

The request body is split into 2 parts:

  1. lines - array of line objects representing existing selection lines that will be updated. Each object consists of:
    • line - [String] Selection line identifier.
    • quantity - [Integer] Could be 0.
  2. items - array of item objects representing items that will be added to the selection. Each object consists of:
    • item - [String] Shop item identifier (pdi_id-psi_id).
    • quantity [Integer] Must be greater than 0.

"items" cannot contain items that are already part of the existing selection line - in such case, line quantity should be increased using "lines"

Example request body

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "lines": [ { "line": "501272086e441ac629c578f5dea8d7b79f03b4c1", "quantity": 1 }, { "line": "7a5e4bd0d41684a550c772eb681299104b06c1d4", "quantity": 0 } ], "items": [ { "item": "5-6", "quantity": 1 }, { "item": "12345-6789", "quantity": 2 } ] }

Response codes#

  • 200 OK - when the request is successful and items have been updated successfully, even partially.
  • 400 Bad Request - when the request body is malformed or missing required fields, such as the items array or itemId and quantity fields in each item.
  • 404 Not Found - when one or more items in the request body cannot be found in the basket.
  • 406 Not Acceptable - when validations fail, e.g. an attempt to add an item that is already part of the selection and line update should be used.

Partial success is possible if request payload validation passes but for some reason line update or selection item addition fails. In such case selection response will contain additional field errors that will contain a list of failed operations.

Note that we don’t have a detailed reason of failure of either line update or item addition, because our service returns only boolean result.

For more information about error handling and responses, please refer to our Checkout API documentation.

Location#

The response from the api contains a location, the visitors country. This is detected from the IP address with Geo-IP. This usually works, but sometimes it does not. For example if you are shopping on a satellite phone. You cannot buy anything until you set a country. Then it will look like this:

1 2 3 4 5 6 7 8 "location": { "country": null, "name": "", "state": null, "stateName": "", "eu": false, "shipTo": false }

The store may not ship to all countries. In this case, shipTo is false. And you cannot buy anything until you set a different country.

1 2 3 4 5 6 7 8 "location": { "country": "AX", "name": "Aland Islands", "eu": false, "state": null, "stateName": "", "shipTo": false }

We do not detect state from Geo-IP. But we need it for the countries that have states (in the JSON datas "countries") because some countries that have states use state-specific tax. This sets country + state:

PUT https://example.com/api/checkout/countries/us/states/ca

1 2 3 4 5 6 7 8 "location": { "country": "US", "name": "United States", "eu": false, "state": "CA", "stateName": "California", "shipTo": true }

PUT https://example.com/api/checkout/countries/PL

This is probably the most common, no states but shipTo is true:

1 2 3 4 5 6 7 8 "location": { "country": "PL", "name": "Poland", "eu": true, "state": null, "stateName": "", "shipTo": true }

If your code are connecting to the API from a server, instead of from the visitors browser, our Geo-IP lookup will always use the IP of your server. You need to have Geo-IP or a similar solution on your end, and tell the API what country the visitor is from.

Address search for Klarna Invoice payments#

POST https://example.com/api/checkout/address-search

1 2 3 4 { "identityNumber": "410321-9202", "paymentMethod": "klarna-invoice" }

Response

1 2 3 4 5 6 7 8 9 10 11 { "token": "3uqv8uq1ubltkppcmv4862e9j4", "address": { "firstName": "Testperson-se", "lastName": "Approved", "address1": "St\u00e5rgatan 1", "zipCode": "12345", "city": "Ankeborg", "country": "SE" } }

This is specifically for klarna invoice, in sweden. 410321-9202 is a personal idenitity number for a klarna test customer.

Localization#

The language is set automatically using GEO IP. The API returns data localized to the selected language.

You can change the language with:

PUT /languages/de

Or with the optional language field when you change country:

1 2 3 4 PUT /countries/se { "language": "sv" }

The response JSON contains a language object, and also a list of languages, and language fields on the location and countries:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 { "location": { "country": "DE", "name": "Germany", "eu": true, "language": "de", "state": null, "stateName": "", "shipTo": true }, "language": { "language": "de", "name": "Deutch" }, ... "countries": [ { "country": "AF", "name": "Afghanistan", "eu": false, "language": "en" }, "..." { "country": "DE", "name": "Germany", "eu": true, "language": "de" }, "..." { "country": "SE", "name": "Sweden", "eu": true, "language": "sv" }, { "country": "CH", "name": "Switzerland", "eu": false, "language": "en" }, "..." ], "languages": [ { "language": "de", "name": "Deutch" }, { "language": "en", "name": "English" }, { "language": "sv", "name": "Svenska" } ] }

The languages array is the possible languages, sorted by name. Use the language as the ID for selecting a language with the PUT /languages/{language}. If Centra is not localized, there will be only one language.

countries have a new language field, the default language for that country. Not sure if you have any use for this.

The language object is the selected language. This is what changes with PUT /languages/{language}

The location.language is just copied from countries. It is not the selected language.

Centra setup:#

In Centra you add new languages with System -> Languages. The "Language code" in Centra is the language id in the API.

The main Centra language (for not localized data) is called en and English by default, this is set up in the API plugin. For example, on a Swedish webshop the Centra data is probably in Swedish so it could make sense to set it as sv Swedish there.

User accounts#

Register:

  • POST register
  • POST login/{email}
  • POST logout

Change the registered address, email, password:

  • PUT address
  • PUT email
  • PUT password

Get pervious orders:

  • POST orders

Password reset email sending and use:

  • POST password-reset-email/{email}
  • POST password-reset

Reminder of how the errors work in the API: If the response does not contain an "errors" object, there are no errors, the operation was successful. This is important here, for example the register operation does not return "register=success". (you also get a return code over 400 for errors)

When you are logged in, the response JSON contains a "loggedIn" object with the customers address. This is the data we have in Centra today, we have 1 address and no separate shipping addresses. The login is connected to the token:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "token": "cstvqilgkdkoijj8ac9fm9o0g0", ... "loggedIn": { "email": "hello@example.com", "firstName": "Hello", "lastName": "World", "address1": "Address One", "address2": "Address Two", "zipCode": "12345", "city": "add", "state": "", "country": "SE", "phoneNumber": "123456789" } }

3 operations will login:

  • POST login/{email}
  • POST register
  • POST password-reset (this is used when you click on the link in the password reset email)

When you are logged in, the items you add to your selection are saved to your account. If you log in on a different computer you have the same items.

If you have items in the selection before you login, they are added to your saved selection. So it will contain your previously saved items plus these new ones.

If you log out, the login is removed from the token, and the selection is empty.

Please see the swagger for the input fields.

Some operations are only allowed if you are logged in. If you are not, they will respond with http code 403 Forbidden and

1 2 3 4 5 { "errors": { "accessRight": "denied" } }

POST register#

This will register a new customer and login. If all goes well, you get back the regular JSON data with a "loggedIn" object.

Possible errors (there are more than this):

  • response code 409: "email":"already registered"
  • response code 406: "password":"not valid" if Centra thinks the password is not good enough. I dont know what criteria it has.

POST login/{email}#

This will login (yes, really). You get the same regular JSON back as with register.

If it fails you get response code 406 and errors with "password":"incorrect". This happens even if the email is not for a registered customer, this call will not reveal if a customer exists or not. You will find out if you use register or password-reset-email

POST logout#

This just logs out. You get the regular JSON response back, now the "loggedIn" is gone

POST password-reset-email/{email} + POST password-reset#

The first will send an email with a password reset link. You can specify the URI of the link, but the base URL must be set in the Centra checkout API plugin. So you cannot send emails to people that point to other domains.

The second is used when the customer clicks the link in the email. You need to pass on the hash from the link to the API.

Example:

1 2 3 4 POST /password-reset-email/same@example.com { "linkUri": "my-uri/with/slashes" }

The "linkUri" is optional, but Centra MUST be setup with a frontend URL.

The email contains this link:

https://mywebshop.example.com/my-uri/with/slashes?id=48&i=8a87a9e9fea3c3cfdea183b281ec43f3

When the customer clicks on it, you need to send the "id" and "i" and "newPassword" to the API:

1 2 3 4 5 6 POST /password-reset { "newPassword":"secret!", "i":"8a87a9e9fea3c3cfdea183b281ec43f3", "id":"48" }

After this the password is changed and you are logged in.

PUT address, PUT email, PUT password#

These just change the address, email and password. I hope the swagger is enough explanation.

POST orders#

This returns a list of the customers previous orders, with shipments and tracking links.

POST "from" and "size" paramters for paging. Without them all orders will be returned. If a customer has a lot of orders it could be slow to get all of them.

The response looks similar to the regular JSON response, except it contains an "orders" object that is an array of orders. And a "ordersPaging" object that keeps track of paging.

Example:

1 2 3 4 5 POST orders { "from":3, "size":2 }

Response:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 { "token": "cstvqilgkdkoijj8ac9fm9o0g0", "orders": [ { "order": "575", "status": "untouched", "statusDescription": "Pending", "message": "Thank you for your order!", "date": "2015-10-20 16:01:34", "currency": "USD", "paymentMethod": "paypal", "paymentMethodName": "P\u00e4jP\u00e4l", "shippingMethod": "", "shippingMethodName": "", "items": [ { "item": "2-1", "product": "2", "brandName": "Brand", "productName": "\u221aEtt", "size": "", "sku": "p1v1", "ean": "", "quantity": 1, "storePickup": false, "line": "17d0eaf6e6a165676cf39222ffc81bff", "priceEach": "$\u00a04\u00a0USD", "priceEachAsNumber": 4, "totalPrice": "$\u00a04\u00a0USD", "totalPriceAsNumber": 4, "priceEachBeforeDiscount": "$\u00a04\u00a0USD", "priceEachBeforeDiscountAsNumber": 4, "anyDiscount": false, "taxPercent": 25, "priceEachWithoutTax": "$\u00a03\u00a0USD", "priceEachWithoutTaxAsNumber": 3.2 } ], "discounts": { "anyDiscount": false, "discount": "$\u00a00\u00a0USD", "discountAsNumber": 0, "automaticDiscounts": [ ], "vouchers": [ ] }, "totals": { "itemsTotalPrice": "$\u00a04\u00a0USD", "itemsTotalPriceAsNumber": 4, "totalDiscountPrice": false, "totalDiscountPriceAsNumber": false, "shippingPrice": "$\u00a099\u00a0USD", "shippingPriceAsNumber": 99, "handlingCostPrice": "$\u00a00\u00a0USD", "handlingCostPriceAsNumber": 0, "totalQuantity": 1, "taxDeducted": "$\u00a0-21\u00a0USD", "taxDeductedAsNumber": -20.6, "taxAdded": false, "taxAddedAsNumber": false, "taxPercent": 0, "grandTotalPrice": "$\u00a082\u00a0USD", "grandTotalPriceAsNumber": 82.4, "grandTotalPriceTax": "$\u00a00\u00a0USD", "grandTotalPriceTaxAsNumber": 0 }, "vatExempt": true, "address": { "email": "fr@example.com", "firstName": "First", "lastName": "Last", "company": "", "address1": "Rue", "address2": "", "zipCode": "75000", "city": "Paris", "state": "Alsace", "country": "FR", "countryName": "France", "phoneNumber": "1234556" }, "shippingAddress": { "email": "fr@example.com", "firstName": "First", "lastName": "Last", "company": "", "address1": "Rue", "address2": "", "zipCode": "75000", "city": "Paris", "state": "Alsace", "country": "FR", "countryName": "France", "phoneNumber": "12345678" }, "giftMessage": null, "shipments": [ ], "currencyFormat": { "currency": "USD", "name": "USD", "prefix": "$ ", "suffix": " USD", "decimalPoint": ".", "decimalDigits": "0", "uri": "usd" } }, { "order": "574", "status": "untouched", "statusDescription": "Pending", "message": "Thank you for your order!", "date": "2015-10-20 15:58:21", "currency": "USD", "paymentMethod": "paypal", "paymentMethodName": "P\u00e4jP\u00e4l", "shippingMethod": "", "shippingMethodName": "", "items": [ { "item": "2-1", "product": "2", "brandName": "Brand", "productName": "\u221aEtt", "size": "", "sku": "p1v1", "ean": "", "quantity": 1, "storePickup": false, "line": "58b36250237a1d67e67fc95ce18c8f7d", "priceEach": "$\u00a04\u00a0USD", "priceEachAsNumber": 4, "totalPrice": "$\u00a04\u00a0USD", "totalPriceAsNumber": 4, "priceEachBeforeDiscount": "$\u00a04\u00a0USD", "priceEachBeforeDiscountAsNumber": 4, "anyDiscount": false, "taxPercent": 25, "priceEachWithoutTax": "$\u00a03\u00a0USD", "priceEachWithoutTaxAsNumber": 3.2 } ], "discounts": { "anyDiscount": false, "discount": "$\u00a00\u00a0USD", "discountAsNumber": 0, "automaticDiscounts": [ ], "vouchers": [ ] }, "totals": { "itemsTotalPrice": "$\u00a04\u00a0USD", "itemsTotalPriceAsNumber": 4, "totalDiscountPrice": false, "totalDiscountPriceAsNumber": false, "shippingPrice": "$\u00a099\u00a0USD", "shippingPriceAsNumber": 99, "handlingCostPrice": "$\u00a00\u00a0USD", "handlingCostPriceAsNumber": 0, "totalQuantity": 1, "taxDeducted": false, "taxDeductedAsNumber": false, "taxAdded": false, "taxAddedAsNumber": false, "taxPercent": 25, "grandTotalPrice": "$\u00a0103\u00a0USD", "grandTotalPriceAsNumber": 103, "grandTotalPriceTax": "$\u00a021\u00a0USD", "grandTotalPriceTaxAsNumber": 20.6 }, "vatExempt": false, "address": { "email": "fr@example.com", "firstName": "First", "lastName": "Last", "company": "", "address1": "Rue", "address2": "", "zipCode": "75000", "city": "Paris", "state": "Alsace", "country": "FR", "countryName": "France", "phoneNumber": "1234556" }, "shippingAddress": { "email": "fr@example.com", "firstName": "First", "lastName": "Last", "company": "", "address1": "Rue", "address2": "", "zipCode": "75000", "city": "Paris", "state": "Alsace", "country": "FR", "countryName": "France", "phoneNumber": "12345678" }, "giftMessage": null, "shipments": [ ], "currencyFormat": { "currency": "USD", "name": "USD", "prefix": "$ ", "suffix": " USD", "decimalPoint": ".", "decimalDigits": "0", "uri": "usd" } } ], "ordersPaging": { "from": 3, "size": 2, "totalSize": 38 }, "products": [ { "product": "2", "name": "\u221aEtt1", "uri": "ett-v1", "sku": "p1v1", "productSku": "p1", "brandName": "Brand", "silkProduct": "1", "silkVariant": "1", "variantName": "V1", "primaryVariant": true, "excerpt": "Min korta display description", "description": "Min l\u00e5nga display description!", "metaTitle": "min display meta title!", "metaDescription": "Min display meta description!", "metaKeywords": "Min display meta keywords!", "stockUnit": "", "harmCode": "", "harmCodeDescription": "", "twilfitMultiSwatch": { "primary": { "color_text": "xzcx", "color_hex": "eqwe", "parent": "asdads", "parent_hex": "adsasd" }, "additional": { "8": { "color_text": "Hejsan", "color_hex": "deadbeef", "color_img": { "type": "image", "url": "http:\/\/7c965611.ngrok.io\/client\/dynamic\/attributes\/concorde32_7663_png.jpg", "width": "24", "height": "24", "mimeType": "image\/jpeg" }, "parent": "parenten", "parent_hex": "cafe" } } }, "price": "$\u00a04\u00a0USD", "priceBeforeDiscount": "$\u00a04\u00a0USD", "priceReduction": "$\u00a00\u00a0USD", "priceAsNumber": 4, "priceBeforeDiscountAsNumber": 4, "priceReductionAsNumber": 0, "discountPercent": 0, "showAsOnSale": false, "newProduct": false, "inStock": true, "items": [ { "item": "2-1", "name": "", "ean": "", "sku": "p1v1", "inStock": true } ], "canonicalUri": "vaxt\/buske\/nypon\/ett-v1", "media": { "s_big": [ "https:\/\/7c965611.ngrok.io\/client\/dynamic\/images\/1_7c19db6361-hagebutten_am_strauch-s_big.jpg", "https:\/\/7c965611.ngrok.io\/client\/dynamic\/images\/1_c1d15594e8-nypon_994760a-s_big.jpg", "https:\/\/7c965611.ngrok.io\/client\/dynamic\/images\/1_c1a16a4381-casio_exilim_ex-z1050_nypon-s_big.jpg" ] }, "relatedProducts": [ { "product": "5", "relation": "standard" }, { "product": "4", "relation": "variant" }, { "product": "3", "relation": "silkProduct" }, { "product": "37", "relation": "silkProduct" }, { "product": "37", "relation": "display" } ] } ], "location": { "country": "US", "name": "United States", "eu": false, "language": "en", "state": null, "stateName": "", "shipTo": true }, "language": { "language": "en", "name": "English" }, "loggedIn": { "email": "same@example.com", "firstName": "H\u00e9ll\u00f6 \u30d7\u30ec\u30a4\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3", "lastName": "W\u00f6\u00aeld \u0625\u0646 \u0634\u0627\u0621 \u0627\u0644\u0644\u0647\u200e", "address1": "Address One", "address2": "Address Two", "zipCode": "12345", "city": "add", "state": "", "country": "US", "phoneNumber": "123456789" } }

The orders do have shipments on them, although not in the example abve. If an order is shipped, it has an array of shipments in "shipments" like this:

1 2 3 4 5 6 7 8 9 10 "shipments": [ { "shippedDate": "2017-08-04 16:22:20", "carrier": "UPS", "service": "Ground", "trackingId": "1Z123123123123", "trackingUrl": "http://example.com/tracking?1Z123123123123" } ],