API Development
JSON Web Token
API integration is managed by encrypted JSON Web Token (JWT). JWT is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims.
A restaurant owner (RO) logs into the Restaurant Portal and is provided a JWT. The JWT authorizes the RO to fetch data that it owns e.g. all deliveries for today.
The portal shows all available services for the current RO e.g. Reports, Orders and Dashboard services. Services provided from the Plugin Registry (PR) are unique for each brand. The PR returns a list of Plugin Objects (PO) which JavaScript bundle URL:
[
{
"code": "HELLO_WORLD",
"route": "hello-world",
"bundleUrl": "https://example.com/app.{bundle}.js"
}
]
+------------+ +--------------------+
| | -------> | Plugin Registry |
| | +--------------------+
| | Reports Plugin URL | | |
| Web |<---------------------+ | |
| Client | Orders Plugin URL | |
| |<------------------------+ |
| | Dashboard Plugin URL |
| |<---------------------------+
+------------+
| | | GET https://plug.rps.com/res/{id}/reports/revenue +-------------+
| | +---------------------------------------------------->| Reports API |
| | +-------------+
| | GET https://oapi.domain.com/res/{id}/orders/today +-------------+
| +------------------------------------------------------->| Orders API |
| +-------------+
| GET https://board.food.com/res/{id}/dashboard +---------------+
+-------------------------------------------------------->| Dashboard API |
+---------------+
JSON Web Key
A JSON Web Key (JWK) is a JSON data structure that represents a cryptographic key. The JWK permits fast local verification of the transmitted JWT for each Portal Service API.
Public Keys
Restaurant Portal operates in three different environments, each environment has a unique JWK:
EU
| Environment | Key | Expiry (seconds) |
|---|---|---|
| Staging | https://0phew9ltyk.execute-api.eu-west-1.amazonaws.com/stg/v2/jwk.json | 3600 |
| Production | https://z2ib6nvxrj.execute-api.eu-west-1.amazonaws.com/prd/v2/jwk.json | 3600 |
APAC
| Environment | Key | Expiry (seconds) |
|---|---|---|
| Production | https://i2i8h1fo03.execute-api.ap-southeast-1.amazonaws.com/prd/v2/jwk.json | 3600 |
MENA
| Environment | Key | Expiry (seconds) |
|---|---|---|
| Production | https://z2ib6nvxrj.execute-api.eu-west-1.amazonaws.com/prd/v2/jwk.json | 3600 |
LATAM
| Environment | Key | Expiry (seconds) |
|---|---|---|
| Production | https://dlbjm8jos6.execute-api.us-east-1.amazonaws.com/prd/v2/jwk.json | 3600 |
RESTful paths
Recommendation: Brands developing a Portal Service API ideally should follow a RESTful pattern for paths e.g. /restaurants/{platformId}/resourceName.
Confirm ownership of resource
After successful verification of the JWT, when available, the platformId in the incoming request should be compared to the identifiers in the JWT authSchema property:
{
"country": "KW",
"user": {
"locale": "en_GB",
"name": "Restaurant Owner",
"email": "[email protected]",
"userId": "162",
"operatorCode": "de-15320-lh2"
},
"version": "1",
"authSchema": {
"restaurants": [
{
"id": "3456", // 9Cookies Go identifier
"platforms": [ // Platform(s) the current restaurant is attached to
{
"restaurantId": "100931123n",
"platformId": "LH_DE", // Global Entity Id
"platformKey": "LH_DE"
},
{
"restaurantId": "987hn423n",
"platformId": "PDE_DE", // Global Entity Id
"platformKey": "PDE_DE"
}
]
}
]
},
"iat": 1523881223,
"exp": 1523883023,
"iss": "portalAuth",
"sub": "1"
}
// e.g. Path: /restaurant/100931123n/reports
// e.g. Header: X-Platform-Id: 100931123n
const platformId = "987hn423n";
const payload = jwt.verify(token, jwks);
const authSchema = JSON.parse(payload['authSchema']);
// Look up restaurant by platformId.
let restaurant;
authSchema.restaurants.find(r => {
return restaurant = r.platforms.find(p => p.platformId == platformId);
});
if (restaurant) {
// The requester is permitted to access to the requested resource.
}
Available platforms with platformKeys
platformId maps directly to Global Entity IDs as described by Data Warehouse and Data Fridge teams.
| Platform | platformKey | platformId (Global Entity Id) |
|---|---|---|
| Mjam | MJM_AT |
MJM_AT |
| Pizza-Online.fi | PO_FI |
PO_FI |
| OnlinePizza.se | FO_OP (OP_SE*) |
OP_SE |
| YoGiYo | YO_KR |
YO_KR |
| DameJidlo.cz | DJ_CZ |
DJ_CZ |
| PizzaPortal | PPP_PL |
PPP_PL |
| Otlob | HF_EG |
HF_EG |
| NetPincer.hu | NP_HU |
NP_HU |
* Due to the migration of OnlinePizza to the Foodora stack, you also need to support the legacy platformKey OP_SE for a limited time.
Talabat
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| United Arab Emirates | TB_AE |
TB_AE |
| Bahrain | TB_BH |
TB_BH |
| Jordan | TB_JO |
TB_JO |
| Kuwait | TB_KW |
TB_KW |
| Oman | TB_OM |
TB_OM |
| Qatar | TB_QA |
TB_QA |
| Saudi Arabia | TB_SA |
TB_SA |
foodonclick
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| United Arab Emirates | FOC_AE |
FOC_AE |
| Jordan | FOC_JO |
FOC_JO |
| Lebanon | FOC_LB |
FOC_LB |
| Oman | FOC_OM |
FOC_OM |
| Qatar | FOC_QA |
FOC_QA |
| Saudi Arabia | FOC_SA |
FOC_SA |
PedidosYa
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| Argentina | PY_AR |
PY_AR |
| Bolivia | PY_BO |
PY_BO |
| Chile | PY_CL |
PY_CL |
| Ecuador | PY_EC |
PY_EC |
| Mexico | PY_MX |
PY_MX |
| Panama | AP_PA |
AP_PA |
| Puerto Rico | PY_PR |
PY_PR |
| Paraguay | PY_PY |
PY_PY |
| Uruguay | PY_UY |
PY_UY |
| Venezuela | PY_VE |
PY_VE |
ClickDelivery
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| Colombia | CD_CO |
CD_CO |
| Ecuador | CD_EC |
CD_EC |
| Greece | CD_GR |
CD_GR |
| Peru | CD_PE |
CD_PE |
Domicilios
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| Colombia | CD_CO |
CD_CO |
| Ecuador | CD_EC |
CD_EC |
| Greece | CD_GR |
CD_GR |
| Peru | CD_PE |
CD_PE |
Foodora
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| Austria | FO_AT |
FO_AT |
| Canada | FO_CA |
FO_CA |
| Finland | FO_FI |
FO_FI |
| Germany | FO_DE |
FO_DE |
| Norway | FO_NO |
FO_NO |
| Sweden | FO_SE |
FO_SE |
Foodpanda
| Country | platformKey | platformId (Global Entity ID) |
|---|---|---|
| Bangladesh | FP_BD |
FP_BD |
| Bulgaria | FP_BG |
FP_BG |
| Hong Kong | FP_HK |
FP_HK |
| Malaysia | FP_MY |
FP_MY |
| Pakistan | FP_PK |
FP_PK |
| Philippines | FP_PH |
FP_PH |
| Romania | FP_RO |
FP_RO |
| Singapore | FP_SG |
FP_SG |
| Taiwan | FP_TW |
FP_TW |
| Thailand | FP_TH |
FP_TH |
Default behaviour
Plugin Service APIs that do not require explicit platformId in requests can assume the first platform to be the "active" platform.
CORS
All endpoints must return 200 for OPTIONS requests with following headers
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Allow-Methods: OPTIONS,GET
Access-Control-Allow-Credentials: false
Additionally, all non OPTIONS methods must also include
Access-Control-Allow-Origin: *
Reports
The portal supports a number of reporting views, as long as the format below is respected. Integrating new reporting endpoints developed by brands is not currently automated and requires some brief collaboration by the Portal team.
Responses must support the following schema:
{
"data":[{
"data":{ ... },
"compare": { ... }
}]
}
The top level data object contains two properties: data and compare. The later is an optional property reserved for comparisons, and only supported via CSV exports.
The keys in the former data property are decided by the developer. For a new report to be included in the portal, the developer must share the English names of each property. If the property represents a unit, that too must be described. Currently supported units
Supported units
All units are browser locale specific
| Unit | Description | Input | Render e.g. |
|---|---|---|---|
DATE |
Full date | 2017-10-25 | 25/10/2017 |
DATE_MONTH |
Month | 2017-10-01 | October |
DATE_YEAR |
Year | 2017-01-01 | 2017 |
CURRENCY |
Restaurant currency, symbol only displayed in table header | 100 | 100 |
Table report
Performance report
GET /v1/restaurants/{id}/reports/orders/day?from=2017-08-01&to=2017-08-01
{
"data":[{
"data":{
"date":"2017-08-01",
"orderCount":62,
"revenue":1031.65
}
},
{
"data": {
"date": "2017-08-02",
"orderCount":53,
"revenue":877.8
}
}]
}
Rendering
| Date | Orders | Sales (€) |
|---|---|---|
| 2017-08-01 | 62 | 1031.65 |
| 2017-08-02 | 53 | 877.8 |

List report
Popular dishes
GET /v1/restaurants/6482/reports/dishes?from=2017-08-01&to=2017-10-31
{
"data":[{
"data":{
"name":"Cheeseburger",
"orderCount":351,
"revenue":1158.3
}
},
{
"data":{
"name":"Pommes Frites",
"orderCount":221,
"revenue":140.6
}
}]
}
Rendering
| Name | Order Amount/Revenue |
|---|---|
| Cheeseburger | 351 1158.3 |
| Pommes Frites | 221 140.6 |

Comparison report
GET
/v1/restaurants/{id}/reports/orders/day?from=2017-08-01&to=2017-08-01&compare[from]=2017-07-31&compare[to]=2017-07-31
{
"data": [
{
"data": {
"date": "2017-08-01",
"orderCount": 62,
"revenue": 1031.65
},
"compare": {
"date": "2017-07-31",
"orderCount": 56,
"revenue": 823.9
}
}
]
}
Rendering
Rendered as CSV file:
day,orders,sales,day_comparison,orders_comparison,sales_comparison
"2017-08-01","62","1031.65","2017-07-31","56","823.9"
Responses
Successful
{
"data": {
}
}
Error
400
{
"state": "ERROR",
"code": "OUT_OF_RANGE",
"message": "You can only select 31 days"
}
{
"state": "ERROR",
"code": "VALIDATION_ERROR",
"message": "Error",
"errors": [{
"field": "id",
"code": "FIELD_CANNOT_BE_EMPTY",
"message": "id cannot be empty",
}]
}
{
"state": "ERROR",
"code": "VALIDATION_ERROR",
"message": "Error",
"errors": [{
"field": "from",
"code": "FIELD_CANNOT_BE_EMPTY",
"message": "from cannot be empty",
}, {
"field": "to",
"code": "FIELD_CANNOT_BE_EMPTY",
"message": "to cannot be empty",
}]
}
401
No token passed
{
"state": "ERROR",
"code": "TOKEN_INVALID",
"message": "Authorization failed, invalid authorization header (did you forget Bearer?)"
}
Unsupported token
{
"state": "ERROR",
"code": "TOKEN_INVALID",
"message": "Authorization failed, provided token cannot be verified",
}
Token expired
{
"state": "ERROR",
"code": "TOKEN_EXPIRED",
"message": "Authorization failed, token expired",
}
403
{
"state": "ERROR",
"code": "FORBIDDEN",
"message": "Forbidden"
}
500
{
"state": "ERROR",
"code": "INTERNAL_ERROR",
"message": "Unknown error"
}