{
"name": "Acme Stores"
,"backoffice_domain": "acmestores.shopoola.com"
,"first_name": "Saurabh"
,"last_name": "Nanda"
,"email": "saurabhnanda@gmail.com"
,"phone": "+91934234234"
}
This is our “minimum viable” JSON API spec for a hypothetical multi-tenant shopping cart application.
Every write to the Postgres DB should result in a corresponding entry in the audit_logs table
Common format for validation errors
API endpoints related to tenant management.
Create a new tenant in an inactive state.
Validations to be implemented:
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.)
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).
| 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"
}
| Headers | |
|---|---|
| Content-Type | application/json |
| Location | /tenants/1 |
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"
}
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
| Parameters | |||
|---|---|---|---|
tenant_id |
number |
the tenant ID |
|
| Headers | |
|---|---|
| session_id | encrypted |
| Headers | |
|---|---|
| Content-Type | application/json |
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"
}
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
inactive state. It’s an error if the tenant is in any other state.owner object should be present and valid: username, password, password_confirmation, first_name, last_nameThe 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
| 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"
}
}
| Headers | |
|---|---|
| Content-Type | application/json |
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"
}
}
We need to discuss the authentication spec before implementing it. Please refer to https://github.com/vacationlabs/haskell-webapps/issues/25
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.
| Headers | |
|---|---|
| Content-Type | application/json |
application/json
{
"username": "saurabhnanda"
,"password": "jdslkjsdf"
,"remember_me": true
}
| 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 |
Requires only the cookies. Do we need to do anything with the POST body?
| Headers | |
|---|---|
| Content-Type | application/json |
| Cookie | auth_token=encrypted |
| Headers | |
|---|---|
| Content-Type | application/json |
| Set-Cookie | session_id=encrypted; Secure |
Requires only the cookies. Do we need to do anything with the POST body?
| Headers | |
|---|---|
| Content-Type | application/json |
| Cookie | session_id=encrypted |
| Headers | |
|---|---|
| Content-Type | application/json |
| Set-Cookie | session_id=blank |
| Set-Cookie | auth_token=blank |
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
| 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. |
|
variants.photo_sizes |
string |
The exact geometry (eg. |
|
| Headers | |
|---|---|
| Content-Type | application/json |
| Cookie | session_id=encrypted |
| Headers | |
|---|---|
| Content-Type | application/json |
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"
}
]
}
]
}
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
| 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 |
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 |
| Headers | |
|---|---|
| Content-Type | application/json |
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"
}
]
}
]
Validations
name, description, currency, product_type, variants, variants.name, variants.sku, variants.price, variants.skuvariants.sku should be unique for the tenant.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.advertised_price, if absent (or blank) should automatically be calculated as minimum price of all variants.comparison_price, if absent (or blank) should automatically be calculated as the advertised_pricecost_price, if absent or blank, should be set to NULL in the DBurl_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 DBProduct & variant creation should be wrapped in a DB transaction
| 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"
}
]
}
| Headers | |
|---|---|
| Content-Type | application/json |
| Location | /products/123 |
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"
}
]
}
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.
| 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) |
|
| Headers | |
|---|---|
| Content-Type | image/png |
image/png
(whatever the web server needs to do to send the image)