Create a tenant
Get tenant details
Activate a tenant
Authenticate a user
Refresh a session
Destroy a session
Get a single product
Get a list of products
Create a product
Edit a product
Upload a photo
Remove a photo
Fetch a photo

Shopping Cart


This is our “minimum viable” JSON API spec for a hypothetical multi-tenant shopping cart application.

Common things to be implemented

  • Every write to the Postgres DB should result in a corresponding entry in the audit_logs table

  • Common format for validation errors

    • TODO
Tenants

API endpoints related to tenant management.

Create a tenant

Create a tenant

  • Create a new tenant in an inactive state.

  • Validations to be implemented:

    • backoffice domain should be unique
    • name should be present and be at least 3 characters in length
    • first name & last name should be present
    • email should be present and valid
    • phone should be present and valid (with country code prefix)
  • Send an email with an activation link to the tenant’s email ID (multi-part email with text part, HTML part, and a logo as an attachment, which is referenced inline by the HTML part.)

    • The activation link will contain a unique+random activation-key, which should be stored against the tenant’s row in the DB.
    • We can simply use a UUID for the activation-key.
  • Upon clicking the activation link the tenant will be taken to an account activation UI, which will finally call the tenant activate endpoint (documented below).

REQUEST

Headers
Content-Type application/json
application/json
                        {
"name": "Acme Stores"
,"backoffice_domain": "acmestores.shopoola.com"
,"first_name": "Saurabh"
,"last_name": "Nanda"
,"email": "saurabhnanda@gmail.com"
,"phone": "+91934234234"
}

                      

RESPONSE

Headers
Content-Type application/json
Location /tenants/1
201 application/json
                      {
  "id": 1,
  "created_at": "iso8601timestamp",
  "updated_at": "iso8601timestamp",
  "first_name": "Saurabh",
  "last_name": "Nanda",
  "email": "saurabhnanda@gmail.com",
  "phone": "+91 982347982374",
  "status": "active",
  "owner_id": 0,
  "backoffice_domain": "acmestores.shopoola.com"
}
                    
Get tenant details

Get tenant details

  • This is not a publicly available API. This needs some kind of authorization, which is why we need the session_id cookie to be present.

  • Validations

    • The tenant ID in the URL should match the tenant ID of the currently signed-in user, else this shold result in a 401
    • In the future, we can extend this to look at the role of the current user. If it is a super-privileged role, then we may allow access to any tenant record via this API.

REQUEST

Parameters
tenant_id number

the tenant ID

Headers
session_id encrypted

RESPONSE

Headers
Content-Type application/json
200 application/json
                      {
  "id": 1,
  "created_at": "iso8601timestamp",
  "updated_at": "iso8601timestamp",
  "first_name": "Saurabh",
  "last_name": "Nanda",
  "email": "saurabhnanda@gmail.com",
  "phone": "+91 982347982374",
  "status": "active",
  "owner_id": 0,
  "backoffice_domain": "acmestores.shopoola.com"
}
                    
Activate a tenant

Activate a tenant

  • Change the tenant’s status in the DB from inactive to active. Remove the activation-key from the DB once successfully activated.

  • Validations to be implemented

    • Tenant ID and activation-key should correspond.
    • Tenant should be only in the inactive state. It’s an error if the tenant is in any other state.
    • Following fields in the nested owner object should be present and valid: username, password, password_confirmation, first_name, last_name
  • The nested owner resource will get stored in the users table, with tenants.owner_id pointing to the newly created user.

  • This will result in multiple reads & writes to the DB, therefore needs to be wrapped in a DB transaction

REQUEST

Parameters
tenant_id number

the tenant ID

Headers
Content-Type application/json
application/json
                        {
"activation_key": 2343-32432-3434-3443"
,"owner": {
  "username": "saurabhnanda@gmail.com"
  ,"password": "fklgfjgfgfdgl"
  ,"password_confirmation": "lkjdflgkjdfg"
  ,"first_name": "Saurabh"
  ,"last_name": "Nanda"
}
}


                      

RESPONSE

Headers
Content-Type application/json
201 application/json
                      {
"tenant": {
  "id": 1
  ,"created_at": "2016-10-04T12:30:00+0000"
  ,"updated_at": "2016-10-04T12:30:00+0000"
  ,"name": "Acme Stores"
  ,"backoffice_domain": "acmestores.shopoola.com"
  ,"first_name": "Saurabh"
  ,"last_name": "Nanda"
  ,"email": "saurabhnanda@gmail.com"
  ,"phone": "+91934234234"
  ,"status": "active"
  ,"owner_id": 1
}
,"owner": {
  "id": 1
  ,"created_at": "2016-10-04T12:30:00+0000"
  ,"updated_at": "2016-10-04T12:30:00+0000"
  ,"username": "saurabhnanda@gmail.com"
  ,"first_name": "Saurabh"
  ,"last_name": "Nanda"
  ,"status": "active"
}
}


                    
authentication

We need to discuss the authentication spec before implementing it. Please refer to https://github.com/vacationlabs/haskell-webapps/issues/25

Authenticate a user

Authenticate a user

If remember_me is true then sends back a non-expiring auth_token (as a cookie) that can be reused to generate the session_id, else just the session_id as a cookie.

REQUEST

Headers
Content-Type application/json
application/json
                        {
"username": "saurabhnanda"
,"password": "jdslkjsdf"
,"remember_me": true
}


                      

RESPONSE

Headers
Content-Type application/json
Set-Cookie auth_token=encrypted; Expires=01 Jan, 2018 00:00:00 GMT; Secure
Set-Cookie session_id=encrypted; Secure
Refresh a session

Refresh a session

Requires only the cookies. Do we need to do anything with the POST body?

REQUEST

Headers
Content-Type application/json
Cookie auth_token=encrypted

RESPONSE

Headers
Content-Type application/json
Set-Cookie session_id=encrypted; Secure
Destroy a session

Destroy a session

Requires only the cookies. Do we need to do anything with the POST body?

REQUEST

Headers
Content-Type application/json
Cookie session_id=encrypted

RESPONSE

Headers
Content-Type application/json
Set-Cookie session_id=blank
Set-Cookie auth_token=blank
Products
Get a single product

Get a single product

NOTE: There are two ways to design this API. Please refer to issue #9 and issue #10 for a complete discussion. The sample response in this documentation is for: GET /products/1?fields=name,currency,advertised_price,photos,variants.name,variants.sku,variants.photos&photo_sizes=100x100,300x250&variants.photo_sizes=thumbnail,wide

REQUEST

Parameters
product_id number

the product ID

fields string

which fields should be included in the JSON response. If omitted, all fields visible to the user (based on the authorization) will be included in the JSON.

photo_sizes string

The exact geometry (eg. 100x100 or 300x250) or pre-defined name (eg. thumbnail or wide or tall)

variants.photo_sizes string

The exact geometry (eg. 100x100 or 300x250) or pre-defined name (eg. thumbnail or wide or tall)

Headers
Content-Type application/json
Cookie session_id=encrypted

RESPONSE

Headers
Content-Type application/json
200 application/json
                      {
"name": "Ceramic mug"
,"currency": "INR"
,"advertised_price": 129.50
,"photos": [
  {
    "100x100": "http://somethin.com/a/b/c/100x100.png"
    ,"300x250": "http://somethin.com/a/b/c/300x250.png"
  }
]
,"variants": [
  {
    "name": "Red color"
    ,"sku": "MUGRED"
    ,"photos": [
      {
        "thumbnail": "http://somethin.com/a/b/c/thumbnail.png"
        ,"wide": "http://somethin.com/a/b/c/wide.png"
      }
    ]
  }
  ,{
    "name": "Blue color"
    ,"sku": "MUGBLUE"
    ,"photos": [
      {
        "thumbnail": "http://somethin.com/a/b/c/thumbnail.png"
        ,"wide": "http://somethin.com/a/b/c/wide.png"
      }
    ]
  }
]
}


                    
Get a list of products

Get a list of products

TODO: There are two ways to design this API. Please refer to issue #9 and issue #10 for a complete discussion. The sample response in this documentation is for: GET /products?fields=name,currency,advertised_price,variants.name,variants.sku

REQUEST

Parameters
ids string

comma separated list of specific product IDs required in the response

q string

full-text search over all relevant fields of the product

title string

filter by product title

sku string

filter by product/variant SKU code

type string

filter by product type

tags string

filter by list of tags (should be comma separated). TODO: Should products return EACH tag or ANY tag?

created_at_min string

filter product created on/after this timestamp (specified in iso8601 format)

created_at_max string

filter product created before this timestamp (specified in iso8601 format)

updated_at_min string

filter product updated on/after this timestamp (specified in iso8601 format)

updated_at_max string

filter product updated before this timestamp (specified in iso8601 format)

limit string

filter product updated before this timestamp (specified in iso8601 format)

limit number

number of results to show. Should be less than 50.

offset number

starting position in the result list (maps to limit/offset in SQL)

orderby string

can be any valid field accepted by the fields parameter, concatenated by .asc or .desc, eg. created_at.asc. Default is updated.desc

fields string

which fields should be included in the JSON response. If omitted, all fields visible to the user (based on the authorization) will be included in the JSON.

Headers
Content-Type application/json
Cookie session_id=encrypted

RESPONSE

Headers
Content-Type application/json
200 application/json
                      [
{
  "name": "Ceramic mug"
  ,"currency": "INR"
  ,"advertised_price": 129.50
  ,"variants": [
    {
      "name": "Red color"
      ,"sku": "MUGRED"
    }
    ,{
      "name": "Blue color"
      ,"sku": "MUGBLUE"
    }
  ]
}
,{
  "name": "Water bottle"
  ,"currency": "INR"
  ,"advertised_price": 49.00
  ,"variants": [
    {
      "name": "Plain"
      ,"sku": "BTLP"
    }
    ,{
      "name": "Printed"
      ,"sku": "BTLDSN"
    }
  ]
}
]


                    
Create a product

Create a product

  • Validations

    • Required fields: name, description, currency, product_type, variants, variants.name, variants.sku, variants.price, variants.sku
    • At least one variant should be present
    • variants.sku should be unique for the tenant.
    • If product_type=physical then variants.weight_in_grams and variants.weight_display_unit are required. On the other hand, if product_type=digital then the weight related field should NOT be present. Sending weight for a digital product should be an error. And NOT sending weight for a physical product should also be an error.
    • Currency at the product level and the variant level should be the same
    • Optional field: advertised_price, if absent (or blank) should automatically be calculated as minimum price of all variants.
    • Optional field: comparison_price, if absent (or blank) should automatically be calculated as the advertised_price
    • Optional field: cost_price, if absent or blank, should be set to NULL in the DB
    • Optional field: url_slug, if absent or blank, should be set to the parameterized/sluggified version of the product name. eg. Ceramic mug => ceramic-mug. The url_slug should be unique for the tenant. So, if an automatically generated URL slug is not unique, a number should be suffixed to the slug to make it unique. If a URL slug specified in the the JSON is not unique, then it should result in a validation error.
    • properties is a free from key-value pair which will be stored in the corresponding JSONB field in the DB
  • Product & variant creation should be wrapped in a DB transaction

REQUEST

Headers
Content-Type application/json
session_id encrypted
application/json
                        {
"name": "Ceramic mug"
,"description": "Very beautiful ceramic mug with flower prints"
,"currency": "INR"
,"advertised_price": 129.50
,"comparison_price": 12.50
,"cost_price": 100
,"product_type": "physical"
,"properties": {
"Ideal for": "Gifting"
,"Breakable": "Yes"
}
,"variants": [
{
  "name": "Red color"
  ,"sku": "MUGRED"
  ,"currency": "INR"
  ,"price": 129.50
  ,"weight_in_grams": 300
  ,"weight_display_unit": "grams"
}
,{
  "name": "Blue color"
  ,"sku": "MUGBLUE"
  ,"price": 132
  ,"weight_in_grams": 300
  ,"weight_display_unit": "grams"
}
]
}


                      

RESPONSE

Headers
Content-Type application/json
Location /products/123
200 application/json
                      {
"id": 123
,"tenant_id": 1
,"created_at": "2016-10-10T14:45:23+0000"
,"updated_at": "2016-10-10T14:45:23+0000"
,"name": "Ceramic mug"
,"description": "Very beautiful ceramic mug with flower prints"
,"currency": "INR"
,"advertised_price": 129.50
,"comparison_price": 12.50
,"cost_price": 100
,"product_type": "physical"
,"properties": {
"Ideal for": "Gifting"
,"Breakable": "Yes"
}
,"variants": [
{
  "id": 342
  ,"tenant_id": 1
  ,"product_id": 123
  ,"created_at": "2016-10-10T14:45:23+0000"
  ,"updated_at": "2016-10-10T14:45:23+0000"
  ,"name": "Red color"
  ,"sku": "MUGRED"
  ,"currency": "INR"
  ,"price": 129.50
  ,"weight_in_grams": 300
  ,"weight_display_unit": "grams"
}
,{
  "id": 343
  ,"tenant_id": 1
  ,"product_id": 123
  ,"created_at": "2016-10-10T14:45:23+0000"
  ,"updated_at": "2016-10-10T14:45:23+0000"
  ,"name": "Blue color"
  ,"sku": "MUGBLUE"
  ,"price": 132
  ,"weight_in_grams": 300
  ,"weight_display_unit": "grams"
}
]
}


                    
Edit a product

Edit a product

Photos
Upload a photo

Upload a photo

Remove a photo

Remove a photo

Fetch a photo

Fetch a photo

Given a geometry (or predefined style), this will either serve a pre-generated photo, or, will resize and crop on-the-fly and serve the freshly generated image.

REQUEST

Parameters
path_segment_1 string

generated when the photo is uploaded. Is generally sent as part of a photo URL provided by some other JSON endpoint (eg. product details endpoint)

path_segment_2 string

generated when the photo is uploaded. Is generally sent as part of a photo URL provided by some other JSON endpoint (eg. product details endpoint)

geometry_or_style string

Needs to be one of the whitelisted set of photo geometries (or styles). Allowing any geometry will open up a DOS attack vector.

original_filename string

generated when the photo is uploaded. Is generally sent as part of a photo URL provided by some other JSON endpoint (eg. product details endpoint)

RESPONSE

Headers
Content-Type image/png
200 image/png
                      (whatever the web server needs to do to send the image)