Fit Analytics Recommendation API Guide

This document serves as a guide for developers using the Fit Analytics Recommendation API. Here's the reference documentation of individual API endpoints: API reference documentation

1. Introduction

The Fit Analytics Recommendation API allows users to programmatically create and manage user profiles, access information needed to provide and configure an interface for a user to provide their information and ultimately provide a size or product recommendation based on the user's input.

The API is mostly built around the JSON-API specification. It only supports one type of response format, which are determined by the content of the Accept header:

  1. the plain JSON mode (Accept: application/json), which is basically a simplified and flattened equivalent of the JSON-API mode (see below), with included objects directly in-place

All API endpoints are secured by a token-based authentication requirement. The token is used in the Authorization header of all requests to endpoints that require authorization.

2. Get Started

Set up an authentication mechanism with asymmetric tokens

First thing we need to set up is an authentication process for the client to call the Fit Analytics back-end API.

So, initially, create an asymmetric set of keys and make the public keys available for us to configure in our back-end. We expect a JWKS (JSON Web Key Sets) in a plain file. If it is served via a public URL, it might look like: https://example.com/.well-known/jwks

The private generated key will then be used to sign the token (JWT) so then the client can make requests to the Fit Analytics back-end. The public keys (JWKS) will then be used to verify the token.

In addition to JWKS, our backend will validate the JWT against validation options that you can customize. They should be JSON like the following:

{
  algorithms: ["RS256"],
  issuer: "https://example.com/",
  audience: "https://widget.fitanalytics.com/api"
  subject: "XXXX",
  maxAge: "360 days",
  clockTolerance: 600,
}

For a full list of the options/requirements can be found here, and the full documentation of the library that we use to validate the JWT here.

IMPORTANT

Inform the Fit Analytics team about the JWKS file and the verification options used so that they're added to the back-end configuration.

JWT signature

The token signature encodes the visitor authentication information and it can be safely stored and used.

As the Fit Analytics back-end is only using asymmetric tokens, the token is generated directly on the client's backend using the private key and the customer-specific info (User ID and session ID). It should contain all information that is needed to authenticate the customer. The API validates the token signature against the public JWKS that has been previously registered with Fit Analytics.

When signing the key, the payload information should include:

  1. sub - subject; User ID (Only if it is a logged in user)
  2. sess - Session ID

And, the signing options should at least contain:

  1. kid - the Key Id


Example

A jsonwebtoken library example in Javascript.

const jwt = require("jsonwebtoken");

const pemString = "-----BEGIN RSA PRIVATE KEY-----\n ... "

const payload = {
  sub: "1000", // User ID
  sess: "m0wIPfpQHGc1QZXfI18juG", // Session ID
};

const jwtOptions = {
  keyid: "xxxxx", // kid
  algorithm: "RS256",
  audience: "https://widget.fitanalytics.com/api",
  issuer: "https://www.example.com",
  expiresIn: "48h",
  notBefore: "-1h",
};

const token = jwt.sign(payload, pemString, jwtOptions);

3. API Usage Examples

This section serves as an introduction to the Fit Analytics Recommendation API. It provides an example of using the Recommendation API as an interface to the backend for building a user interface similar to the current Fit Analytics web-based Fit Finder widget.

1. Authorization

All API endpoints are secured by a token-based authentication requirement. The token is used in the Authorization header of all requests to endpoints that require authorization.

For more information about how to set up the authorization mechanism, see: Get Started section.

2. Load Product Data

Once we have the token, we can fetch some basic product data that will be needed for future requests.

Here's what the request and response could look like for a supported product (example-1150064):

>>>>
GET /api/v1/products/example-1150064?filter[shopLanguage]=en&filter[shopCountry]=GB&filter[shopSizingSystem]=UK
Content-Type: application/json
Authorization: Bearer ...
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "example-1150064",
    "title": " Super Skinny Blazer In Charcoal Jersey",
    "category": "upper",
    "gender": "m",
    "thumbnailUrl": "http://d3p40ik6q6v86a.cloudfront.net/garments/example/300/1150064.jpg"
  }
}

Important field here are the gender and the category. Both of these can and should be used in later requests, for example for filtering the relevant data.

And here's what response would look like for an unsupported product:

>>>>
GET /api/v1/products/example-1061683?filter[shopLanguage]=en&filter[shopCountry]=GB&filter[shopSizingSystem]=UK
Content-Type: application/json
Authorization: Bearer ...
<<<<
Status: 404 Not Found

For unsupported products, a Fit Finder widget link shouldn't be shown, and no recommendation requests should be made.

3. Get User Profile

Next, we check if the user has any existing profiles.

>>>>
GET /api/v1/profiles
Content-Type: application/json
Authorization: Bearer ...
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": [
    {
      "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
      "gender": "m",
      "height": 180, // cm
      "weight": 80, // kg
      "heightDisplayUnits": "imperial", // so the height would be displayed as 5 ft 11 in in the Fit Finder widget
      "weightDisplayUnits": "imperial", // so the weight would be displayed as 12 st 8 lb when in British shop or as 176 lb otherwise
      "upperExplicitPreference": 1,
      "age": 32
      "bodyShape: "M.24.B2.C2",
      "upperReferenceBrand": "tnf",
      "upperReferenceSize": "1430"

    }, {
      "id": "01ARZ3NDEKTSV9G5FAV4RRFFQ6",
      "gender": "w",
      "height": 172, // cm
      "weight": 60, // kg
      "heightDisplayUnits": "metric", // so the height would be displayed as 172 cm in the Fit Finder widget
      "weightDisplayUnits": "metric", // so the weight would be displayed as 60 kg in the Fit Finder widget
      "lowerExplicitPreference": 1,
      "age": "28"
      "bodyShape": "F.20.B1.H2",
      "braSize": "820",
      "lowerReferenceBrand": "levis",
      "lowerReferenceStyle": "200658",
      "lowerReferenceSize": "2848"
    }
  ]
}

As shown in the example, there may be multiple profiles associated with a given customer or session ID. They can be narrowed down by filtering by the gender value of the product:

GET /api/v1/profiles?filter[gender]=m
Content-Type: application/json
Authorization: Bearer ...

Even then, there may be more than one matching profile, so there should be some way to determine which is the relevant one, e.g. by presenting the possible profiles to the user and asking him to choose which one he wants to use. Alternatively, the most recently active profile for a given gender can be selected by setting filter[isPrimary]=true.

It's also possible for there to be no results (404 Not Found). In that case, a new profile can be created later, when the user has opened Fit Finder and entered some info.

4. Get Immediate Recommendation

If the product is supported, we can now try to get an immediate recommendation without requiring the user to open Fit Finder.

>>>>
POST /api/v1/products/example-1150064/recommendations
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "shopLanguage": "en",
    "shopCountry": "GB",
    "shopSizingSystem": "UK",
    "isImmediate": true,
    "manufacturedSizes": {
      "Chest 36in Short": true,
      "Chest 38in Short": true,
      "Chest 40in Short": true,
      "Chest 42in Short": true,
      "Chest 32in Regular": true,
      "Chest 34in Regular": true,
      "Chest 36in Regular": true,
      "Chest 38in Regular": true,
      "Chest 40in Regular": true,
      "Chest 42in Regular": true,
      "Chest 44in Regular": true,
      "Chest 46in Regular": true,
      "Chest 38in Long": true,
      "Chest 40in Long": true,
      "Chest 42in Long": true,
      "Chest 44in Long": true,
      "Chest 46in Long": true
    },
    "profile": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  }
}
<<<<
Status: 201 Created
Content-Type: application/json
{
  "data": {
    "id": "01BRRZB1YTWS8PWVAJFBBRZ04H",
    "shopLanguage": "en",
    "shopCountry": "GB",
    "shopSizingSystem": "UK",
    "isImmediate": true,
    "manufacturedSizes": ..., // same as above
    "flags": {
      "usedUserMeasures": true,
      "usedReferenceItem": false,
      "usedPastPurchases": false
    },
    "profile": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "product":"example-1150064",
    "recommendedSizes": [
      {
        "id": "01C5K4EABC5ZEC6D67AY9BP1JS",
        "name": "Chest 40in Long",
        "abbreviation": "40 Long",
        "score": 76,
        "isOutOfScale": false,
        "isInStock": false,
        "size": {
            "id": "227",
            "name": "40",
            "sizingSystem": "de"
        },
      },
      {
        "id": "01C5K4EABCD4B06SV4MJWD412V",
        "name": "Chest 38in Long",
        "abbreviation": "38 Long",
        "score": 24,
        "isOutOfScale": false,
        "isInStock": false,
        "size": {
            "id": "225",
            "name": "38",
            "sizingSystem": "de"
        },
      }
    ]
  }
}

The profile relationship is optional. If it isn't provided, we will attempt to make a recommendation based solely on the user's past purchases that match the target product's category and gender.

4.1 Recommendation errors

If there are no relevant past purchases and either no user profile or only an incomplete one, the response will list what is still missing. For upper-body items, for example, the response could look like this:

Status: 400 Bad Request
Content-Type: application/json
{
  "errors": [
    {
      "status": "400",
      "code": "rec_no_height",
      "title": "Missing profile height",
      "source": {
        "fields": [ "profile.height" ]
      }
    },
    {
      "status": "400",
      "code": "rec_no_weight",
      "title": "Missing profile weight",
      "source": {
        "fields": [ "profile.weight" ]
      }
    },
    ...
  ]
}

For shoes, the height and weight aren't required, but a reference item must be specified:

Status: 400 Bad Request
Content-Type: application/json
{
  "errors": [
    {
      "status": "400",
      "code": "rec_no_ref_item",
      "title": "Missing shoes reference item",
      "source": {
        "fields": [
          "profile.shoesReferenceBrand",
          "profile.shoesReferenceSize"
        ]
      }
    }
  ]
}

5. Body Mass Screen

If a new user with no relevant purchase history clicks on the Fit Finder link, the first screen he sees will be the "body mass screen", where he's prompted to enter his height, weight, and preferred fit.

Once values have been entered for all fields, they can be added to the user profile as follows (making sure to convert the height and weight to metric units!):

>>>>
PATCH /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "height": 180, // cm
    "weight": 80, // kg
    "heightDisplayUnits": "imperial",
    "weightDisplayUnits": "imperial",
    "upperExplicitPreference": -1
  }
}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "gender": "m",
    "height": 180, // cm
    "weight": 80, // kg
    "heightDisplayUnits": "imperial",
    "weightDisplayUnits": "imperial",
    "upperExplicitPreference": -1
  }
}

If there was no existing user profile, a new one must be created:

>>>>
POST /api/v1/profiles
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "gender": "m",
    "height": 180,
    "weight": 80,
    "heightDisplayUnits": "imperial",
    "weightDisplayUnits": "imperial",
    "upperExplicitPreference": -1
  }
}
<<<<
Status: 201 Created
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    ... // same as above
  }
}

Note that the gender must be specified when creating a new profile.

6. Reference Brand Screen

Before we can show the reference brand screen, we'll need to get a list of relevant reference brands. The value for the category and gender filters should correspond to the same values in the original product response.

>>>>
GET /api/v1/brands?filter[category]=upper&filter[gender]=m&filter[shopId]=example&filter[shopLanguage]=en&filter[shopCountry]=GB&fields[brands]=*,isTopReference
Content-Type: application/json
Authorization: Bearer ...
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": [
    {

      "id": "tommy-hilfiger",
      "name": "Tommy Hilfiger",
      "isTopReference": true
    },
    {
      "id": "nike",
      "name": "Nike",
      "isTopReference": true
    },
    {
      "id": "tnf",
      "name": "The North Face",
      "isTopReference": true
    },
    ... // hundreds of brands, e.g. over 1000 for men's tops
  ]
}

The query parameter fields[brands]=*,isTopReference causes the response to include an additional field isTopReference. The seven popular brands that are displayed prominently by the Fit Finder widget on the brands screen have the isTopReference attribute set to true.

7. Reference Style Screen

In the lower-body and footwear Fit Finder widgets, the user can provide more information about his reference item by selecting the garment type (e.g. "slim jeans") or even the model name (e.g. "501" for Levi's or "Air Jordan" for Nike).

As for the other reference screens, we fetch the list of available styles first.

>>>>
GET /api/v1/styles?filter[brand]=levis&filter[category]=lower&filter[gender]=m&filter[shopCountry]=DE&filter[shopLanguage]=de&include=garmentType
Content-Type: application/json
Authorization: Bearer ...

<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": [
    {
      "id": "82931",
      "name": "510",
      "category": "lower",
      "gender": "m",
      "garmentType": {
        "id": "denim_skinny",
        "name": "skinny jeans"
      }
    },
    {
      "id": "82941",
      "name": "511",
      "category": "lower",
      "gender": "m",
      "garmentType": {
        "id": "denim_slim",
        "name": "slim jeans"
      }
    },
    {
      "id": "151361",
      "name": "", // this is a generic style and has no name
      "category": "lower",
      "gender": "m"
      "garmentType": {
        "id": "denim_slim",
        "name": "slim jeans"
      }
    },
    ... // potentially dozens of styles
  ],
}

Here the API return two kinds of style items in the response:

  1. generic style (e.g. sneakers), which look like this: {name: "", gender: "m", garmentType: {name: "sneakers", id: 100}, id: 217191}
  2. specific model styles (e.g. Air Max), which look like this: {name: "Air Max", gender: "m", garmentType: {name: "sneakers", id: 100}, id: 261203}

The specific model styles are a specific subset of generic styles and the UI in the widget works in such that the customer is first presented with the list of generic styles (sneakers, running shoes etc) and when they pick one, they (optionally) get a list of specific models filtered by the garmentType of the previous generic style (ie. sneakersAir Max, Air Jordan etc). In some cases there are only generic styles present.

7. Reference Size Screen

If the user chose a reference brand in the previous screen, we now fetch the list of possible sizes for that brand.

  • the filter[brand] parameter value should be the selected brand ID input from the previous brands screen
  • the filter[category] and filter[gender] should correspond to the viewed product
  • the fields[reference-sizes]=*,components,tags adds size components fields which are relevant for multi-dimensional sizes (more below) and tags are used for the size visual grouping.
>>>>
GET /api/v1/reference-sizes?filter[brand]=river-island&filter[category]=upper&filter[gender]=m&filter[shopLanguage]=en&filter[shopCountry]=GB&include=sizingSystem&fields[reference-sizes]=*,components,tags
Content-Type: application/json
Authorization: Bearer ...

<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": [
    {
      "id": "264-224-DE"
      "name": "42",
      "label": "DE",
      "size": "224",
      "tags": [ "numeric" ],
      "sizingSystem": {
        "name": "DE",
        "id": "de"
      },
      "components": {
        "main": { "code": "42" }
      }
    },
    ...
    {
      "id": "264-226-DE",
      "name": "46",
      "label": "DE",
      "size": "226",
      "tags": [ "numeric" ],
      "sizingSystem": {
        "name": "DE",
        "id": "de"
      },
      "components": {
        "main": { "code": "46" }
      }
    },
    ...
    {
      "id": "264-1428-XX",
      "name": "S",
      "label": "XX",
      "size": "1428",
      "tags": [ "letter" ],
      "sizingSystem": {
        "name": "XX",
        "id": "xx"
      },
      "components": {
        "main": { "code": "S" }
      }
    }
    ... // typically a few dozen sizes (potentially hundreds)
  ]
}

All of the sizes in the above example only have one component, but generally speaking some sizes may have two components, e.g. width (key main) and length for pants:

{
  "id": "264-2984-WL",
  "name": "37/30",
  "label": "WL",
  "size": "2984",
  "tags": [ "numeric" ],
  "sizingSystem": {
    "name": "WL",
    "id": "wl"
  },
  "components": {
    "main": { "code": "37" },
    "length": { "code": "30" }
  }
}

For sizes like these, we currently split the size selection into two steps; we first ask the user to choose the main component, then the secondary component (e.g. the length).

9. Add Reference Item to User Profile

If the user selected a reference brand and size (possibly also a reference style), the user profile can be updated accordingly.

>>>>
PATCH /api/v1/profiles/71FSRgCzcmJmMjLiuyu5CSpyHI
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "lowerReferenceBrand": "levis",
    "lowerReferenceStyle": "200658",
    "lowerReferenceSize": "264-2984-WL"
  }
}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "71FSRgCzcmJmMjLiuyu5CSpyHI",
    "gender": "w",
    "height": 172, // cm
    "weight": 60, // kg
    "heightDisplayUnits": "metric",
    "weightDisplayUnits": "imperial",
    "lowerExplicitPreference": 1
    "lowerReferenceBrand": "levis",
    "lowerReferenceStyle": "200658",
    "lowerReferenceSize": "264-2984-WL"
  }
}

10. Body Shape Screens

For the body shape screens, we request a list of body shapes appropriate for the user's gender, height and weight.

>>>>
GET /api/v1/body-shapes?filter[gender]=m&filter[height]=180&filter[weight]=80
Content-Type: application/json
Authorization: Bearer
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": [
    {
      "id": "M.24.B1.C1",
      "gender": "m",
      "bmi": "24",
      "imageUrl1": "http://media.fitanalytics.com/widget_v2/bodyshapes-20151211/M.24.B1.png",
      "imageUrl2": "http://media.fitanalytics.com/widget_v2/bodyshapes-20151211/M.24.B1.C1.png"
    },
    {
      "id": "M.24.B1.C2",
      "gender": "m",
      "bmi": "24",
      "imageUrl1": "http://media.fitanalytics.com/widget_v2/bodyshapes-20151211/M.24.B1.png",
      "imageUrl2": "http://media.fitanalytics.com/widget_v2/bodyshapes-20151211/M.24.B1.C2.png"
    },
    ... // more body shapes (typically 9 in total)
  ]
}

Each body shape has two image URLs, corresponding to the two steps in the body shape selection process. We first present the user with the set of imageUrl1 images. Once he chooses one of those, we present him with the imageUrl2 images for body shapes matching his choice.

Once an image from the second set has been selected, the user profile can be updated again.

>>>>
PATCH /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "bodyShape": "M.24.B2.C2"

}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "gender": "m",
    "height": 180,
    "weight": 80,
    "heightDisplayUnits": "imperial",
    "weightDisplayUnits": "imperial",
    "upperExplicitPreference": -1
    "bodyShape": "M.24.B2.C2",
    "upperReferenceBrand": "tnf",
    "upperReferenceSize": "1430"
  }
}

11. Age Screen

Here we can simply update the user's profile with the selected age:

>>>>
PATCH /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "age": 32
  }
}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "gender": "m",
    "height": 180,
    "weight": 80,
    "heightDisplayUnits": "imperial",
    "weightDisplayUnits": "imperial",
    "upperExplicitPreference": -1,
    "age": 32
    "bodyShape": "M.24.B2.C2",
    "upperReferenceBrand": "tnf",
    "upperReferenceSize": "1430"
  }
}

12. Bra Size Screen

We request the list of possible bra sizes in a similar way to how we request possible reference sizes.

  • the filter[category]=bra ensures that we get only bra reference sizes
  • the filter[brand]=generic ensures that sizes we get are generic and are not related to any specific brand
  • the fields[reference-sizes]=name,label,components will ensure that we're getting additional attributes like the size component and their individual labels, which might be useful for the UI.
>>>>
GET /api/v1/reference-sizes?filter[category]=bra&filter[gender]=w&filter[brand]=generic&filter[shopLanguage]=en&filter[shopCountry]=GB&fields[reference-sizes]=name,label,components
Content-Type: application/json
Authorization: Bearer ...
<<<<
Status: 200 OK
Content-Type: application/json
{
  "version": "1.0", // the current API version
  "jsonapi": { "version": "1.0" }, // the JSON-API format version
  "data": [
    {
      "id": "0-14441-UK", // internal size ID
      "name": "32/B", // human-readable name
      "label": "UK", // sizing system label
      "components": { // individual components and their labels
        "main": { "code": "32" },
        "cup": { "code": "B" }
      },
      "sizingSystem": "gb" // internal sizing system ID
    },
    ...
    {
      "id": "0-14480-UK",
      "name": "36/DD",
      "label": "UK",
      "components": {
        "main": { "code": "36" },
        "cup": { "code": "DD" }
      },
      "sizingSystem": "gb"
    },
    {
      "id": "0-8721-ES",
      "name": "80/D",
      "label": "ES",
      "components": {
        "main": { "code": "80" },
        "cup": { "code": "D" }
      },
      "sizingSystem": "3b"
    },
    ... // typically a few dozen sizes (potentially hundreds)
  ]
}

Once a bra size has been chosen, we can update the user's profile again:

>>>>
PATCH /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Content-Type: application/json
Authorization: Bearer ...
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "braSize": "0-14480-UK"
  }
}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "gender": "w",
    "height": 172,
    "weight": 60,
    "heightDisplayUnits": "metric",
    "weightDisplayUnits": "imperial",
    "lowerExplicitPreference": 1,
    "age": 28
    "bodyShape": "F.20.B1.H2",
    "braSize": "0-14480-UK",
    "lowerReferenceBrand": "levis",
    "lowerReferenceStyle": "200658",
    "lowerReferenceSize": "2848"
  }
}

13. Result Screen

To get the final recommendation, we make a recommendation request identical to the immediate recommendation request, except that we leave out the "isImmediate" attribute.

14. Details of the Plain-JSON mode

In addition to the JSON-API request and response format, which is specified by the JSON-API specification and indicated by the Content-Type: application/vnd.api+json header, the API also supports the so-called plain-JSON mode. When working in this mode the request/response format is considerably simplifed. The main goal is to simplify development when, for example, the API client implements a graphical interface using the API and doesn't need or expect the exact type specification on every model of the returned response.

When the client sends Accept: application/json and Content-type: application/json headers, the response (and the expected request body) will have a so-called plain-JSON format. The main differences are:

  1. the data object of the request/response directly contains all resource attributes
  2. the type field is not included in the response and is not required in the request body
  3. the resource ID is placed directly in the data object and mixed with other attributes
  4. the resource ID is not required in every request body (unless the operation itself would require it)

The request URI format stays exactly the same as with JSON-API, including special parameters like fields and include.

Here's an example of the simple PATCH request and the response:

>>>>
PATCH /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Accept: application/json
Content-Type: application/json
{
  "data": {
    "height": 180,
    "weight": 80,
    ...
  }
}
<<<<
Status: 200 OK
Content-Type: application/json
{
  "data": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "gender": "m",
    "height": 180,
    "weight": 80,
    ...
  }
}

The plain-JSON mode supports embedded "relationships" and "includes" similar to the JSON-API mode. The main difference is that in the plain-JSON mode, the relationships are plain string IDs placed directly as field values. For example, the profile resource response could look like this:

>>>>
GET /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV
Content-Type: application/json
<<<<
{
  "data": {
    ...
    "upperExplicitPreference": -1,
    "age": 32,
    "upperReferenceBrand": "tnf",
    "upperReferenceSize": "1430"
  }
}

where upperReferenceBrand and upperReferenceSize are resource IDs (and can be used as such in corresponding resource endpoints, e.g. GET /api/v1/brands/tnf). When "includes" are requested, they are placed directly into the corresponding field in the response (as opposed to the JSON-API format, in which they are placed outside the response data object):

>>>>
GET /api/v1/profiles/01ARZ3NDEKTSV4RRFFQ69G5FAV&include=upperReferenceBrand
Content-Type: application/json
<<<<
{
  "data": {
    ...
    "upperReferenceBrand": {
      "id": "tnf",
      "name": "The North Face",
      ...
    },
    ...
  }
}

4. Customer IDs and profiles

Customer identifiers

Our system recognizes three types of customer identifiers:

  1. Shop User ID: represents a customer that is registered with the client and is logged-in to the client's system on the API client (e.g. mobile device). The Shop User ID value should stay the same for the same customer between logins, and between different devices. It must not contain any personal identifiable information like for example an email address. Often referenced as shopUserId in the code or documentation.
  2. Shop Session ID: represents a unique session of the customer, regardless of their logged-in status. It should be present both for guests (ie. anonymous/non-logged-in customers) and for logged-in users. It should be unique for each device the customer uses the client's system on. It should preferably persist on the device between visits/interactions, where and if possible. Often referenced as shopSessionId in the code or documentation.
  3. Internal Session ID: in some cases it makes sense to include the internal Fit Analytics session ID. This ID is usually generated and stored by the Fit Finder widget on the client's website. The most common use-case for including the Internal Session ID is to share sessions between the Fit Finder widget and requests to the Fit Analytics API on the same page. Usually referenced as the sessionId in the code or documentation.

Customer records and profiles

When customers interact with the store, one of the usual scenarios is the following: an anonymous customer begins to browse the client's store and may try to add their inputs for a size recommendation. They're not logged-in at the time so only their session ID is present. Later, they log in and/or register (usually before the checkout) and their shop user ID is added into requests (in the token). Later they may log in and access the store from a different device under the same account and their previous inputs should be present and preferably already used for recommendations. The Fit Analytics service tries to resolve this scenario and many others in the following way:

  • It stores all the customer's input in one or more records. Each customer record may contain one or more profiles. Each profile is assigned a garment gender (where the value is one of: m/w/b/g ) and contains a set of customer's inputs. There's always exactly one profile that is marked as primary within each garment gender group, either explicitly with isPrimary attribute or implicitly by heuristic. The primary profile is used as a default profile in situations where the exact profile ID isn't explicitly specified. It's also useful when the user creates one or more different profiles and is able to switch between them through the user interface.
  • When working with profiles the combination of customer identifiers present in the token determines which records are taken into account. When the Shop User ID is present it will be used for accessing and managing a separate customer record (the "user-record") and when the Shop Session ID or the Internal Session ID is present it will be used for access to another record (the "session-record").
  • When both types of identifiers are present then both types of records are accessed. The system tries to synchronize both records by merging individual profiles and fields and keeping them up-to-date. The exact rules for merging records and profile are relatively complex, however in general the information from "user-record" takes precedence over the information in the "session-record".

5. Purchase events reporting

In many cases, it's useful for your shop to share the sales data with Fit Analytics through reporting individual purchases. For regular website integrations this usually happens on the Order Confirmation Page (OCP). The collected data can be used for calculating the conversion rate and so on. For more information about purchase data sharing see the section Sales Data Exchange of the general documentation.

For API-based integrations the reporting of purchases is also possible, especially in cases when there's an equivalent of the Order Confirmation Page. For such cases, you can a use the Purchase Events API endpoint at /api/v1/events/purchases to send individual purchases to our service.

Request endpoint and structure

POST /api/v1/events/purchases/
Content-Type: application/json
{
  "data": [
    PurchaseEvent,
    PurchaseEvent,
    ...
  ]
}

PurchaseEvent model

All the customer identifiers should be sent as a part of the request in the API authentication token. The purchase events payload should contain only the information about the order and all the products the order contains.

See the full specification at: https://media.fitanalytics.com/resources/api/reference-latest.html#type-purchaseevent

PurchaseEvent {
  product: String,   // required .. product ID, with the client-specific prefix; example: "shop-12345"
  variant: String,   // optional .. variant ID, if specified should be unique for any product ID and size combination, example: "21347722-12"
  ean: String,       // optional .. EAN code of the variant, example: "123456789012"
  order: String,     // required .. unique ID of the order, example: "8346522"
  shopCountry: String, // required .. two-letter country code, example: "GB"
  shopLanguage: String, // required .. two-letter language code, example: "de"
  shopSizingSystem: String, // optional .. code of the shop-specific sizing system, example: "UK"
  purchasedSize: String, // optional .. code of the purchased size, required for garments, example: "M"
  price: Number,     // optional .. purchase price per item, example: 32.99
  currency: String,  // optional .. currency code of the purchase price, example: "EUR"
  quantity: Number,  // optional .. number of items purchase; defaults to 1; should be integer, example: 2
  purchaseDate: DateTime, // optional .. ISO8601 full datetime of the purchase including the timezone or UTC code ("Z" suffix), example: "2021-01-01T20:30:22+01:00", "2021-01-01T21:30:22Z"; if not specified the recorded value will be the time of the request arrival
  platform: String, // optional .. platform identifier, should be one of "app-ios", "app-android", "website"; example: "app-android"
}

Response

{
  "data": null, // .. always null
  "errors": [  // .. array that corresponds to the input array element-wise, `null` means success
    null, 
    { "status": 422, "code": "required_attribute", "title": "Attribute product is required" }
   ],
  "meta": {
    "txid": "73b32c2ec01bb9dd092e6c00", // .. the unique request identifier
    "count": 1 // .. the total count of non-rejected events
  }
}

Example of purchase reporting request

>>>>
POST /api/v1/events/purchases/
Content-Type: application/json
[
  {
    product: "example-355133",
    variant: "43113",
    order: "5642212",
    shopCountry: "DE",
    shopLanguage: "de",
    purchasedSize: "M",
    price: 143.99,
    currency: "EUR",
    quantity: 1,
    purchaseDate: "2021-12-30T20:03:33Z", 
    platform: "app-android",
  }
]
<<<<
Content-Type: application/json
{
  "data": null,
  "errors": [
    null
   ],
  "meta": {
    "txid": "73b32c2ec01bb9dd092e6c00",
    "count": 1
  }
}

6. API policy and backwards compatibility

Wherever possible, our APIs will be maintained in a backwards compatible way. All of our API resources are versioned and the version is specified with the counter prefix in the resource path, e.g. /api/v1/resource. We're currently using prefix v1 everywhere. If it is necessary to change an endpoint in a way that it is not backwards compatible (see Breaking Changes for more info) i.e. it involves a breaking change, a new version of the resource will be created and the old version will be maintained for a specific period.

What CAN change

  • New features that can optionally be used (e.g. optional parameters in the body or new filters) that do not affect the existing behavior of the API. These do not require any updates by the consumers.
  • Fixes and changes to existing APIs that do not require any change from the consumer (e.g. changes in some optional response headers or some optimizations from our end)
  • New optional fields in the response that do not alter the original behavior of the API, but offer some more information if needed.
  • New optional headers that may be used for some additional filtering or triggers.
  • Fixes of bugs and updates to align discrepancies from our existing specifications. In the event that the API is not working as documented or as generally expected, we can fix the API to comply to its original intended behavior. This is not considered a breaking change and may require changes from the consumer. In the event of such a change, the consumers will be duly notified.

What will NOT change

  • API naming conventions. We will not alter the names of existing endpoints, rather add new ones as necessary.
  • API status codes and error codes. We will ensure the existing API behaves as documented.
  • Request body and headers. We will not introduce new required fields or headers in the API.
  • API response format and the mandatory response fields. We will not alter the response format and the mandatory fields already present.

Any change that does not comply to the above will be considered a breaking change.

Breaking changes

Significant changes to the API that would break the implementation of the consumer are considered breaking changes, some of them may be

  • Removing or renaming of API params.
  • Changes in the behavior of an existing endpoint
  • Changes in error codes or HTTP status codes.
  • Deprecating existing endpoints.

Creating new version of a resource is our way of handling of breaking changes. In the event of a breaking change, a deprecation notice along with a relevant migration guide will be published for a seamless transition. During the course of the transition period, we will maintain both the older version and the newer version of the resource until a deprecation date, after which the older version will be unsupported and will eventually be discontinued.