API reference.
Each endpoint of the TUBR Partner API, with its parameters, request body and response.
TUBR helps hospitality businesses predict their sales. With the Partner API, you can bring TUBR's forecasting to your own customers: set up companies and their outlets, keep opening hours and product catalogues in sync, send through orders as they happen, and pull back sales predictions to use in your own product.
Note
Send each request to https://api.prod.tubr.live. The paths on this page include the /v1 version.
To send a test request from your browser, use the API console.
Authentication
Every request needs a bearer token in the Authorization header:
There are two types of token, depending on how you're working with TUBR.
Partner key
You'll get one of these when you sign up as a TUBR partner. It gives you access to all the companies linked to your partner account, along with any outlets you've set up for them. Starts with tbp:
Company token
This is issued to a single company. It only gives access to that company and its own outlets. Starts with tbt:
Quick start
- Become a partner Get in touch and tell us a bit about your platform and the businesses you work with. Once we've reviewed your request, we'll email you a partner key. You'll use it for every step below.
- Register a company Start by setting up the business you're onboarding. All we need is the company's name and a contact phone number. We'll send back a company code, and the company is linked to your partner account straight away. Everything else, from people and outlets to orders and forecasts, lives under this company.
- Invite the team Add the people at the company who'll be using TUBR, such as owners, managers or ops leads. Each person gets an email with a link to sign in, so they can see their forecasts without any extra setup from you.
- Add outlets Next, add the company's venues. You can add a single site or a whole estate in one request. Each outlet gets an outlet code: you can supply your own to match your system, or we'll generate one for you. From this point on, everything is set up per outlet.
- Set opening hours Give each outlet its regular weekly schedule. We treat any time outside these hours as closed, so it's important to set them before you send any sales. Otherwise, orders could land in hours we think the venue wasn't trading.
- Send the product catalog Share each outlet's products and the categories they belong to. We use this catalogue to match up incoming sales, so the more complete it is, the better your forecasts will be.
- Send orders This is where the forecasting really starts. Send sales as they happen, with one product line per order. If you have past sales data, send that too. Our models learn from history, so a good backfill makes a real difference to accuracy from day one. Uploads are processed in a queue, and each one returns a reference you can use to check its progress.
- Get predictions Now for the good part. Fetch forecasts for each outlet, either daily or hourly, broken down by feed type: the sales total, or the number of transactions. Your first forecast will be ready about a day after the first orders arrive, and from then on they'll keep improving as more data comes in.
Companies
Companies are the businesses you bring onto TUBR. Register a new company to get its company code, look up the companies you manage, or deactivate one when you stop working with it.
List companies
GET
/v1/companies
The companies the caller may manage: the ones attached to the calling partner, or a company token's own.
Response 200.
Register a company
POST
/v1/companies
A partner registers a company under its key: the company is attached to the partner from the start and the key manages it. A company token is refused. The company code is generated and returned; keep it, every outlet is created under it. Rate limited per partner.
Request body (application/json).
Field | Type | Description |
|---|---|---|
company_name | string | Required. Up to 100 characters. |
phone | string | Up to 20 characters. |
Response 201.
Read a company
GET
/v1/companies/{company_code}
One company by code, among the ones the caller may manage. A company outside the caller's scope is a 404, indistinguishable from one that does not exist.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | path | string | Required. |
Response 200.
Deactivate a company
DELETE
/v1/companies/{company_code}
Soft-deletes a company attached to the calling partner. Its outlets are kept, but drop out of the API with it.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | path | string | Required. |
Response 204. No body. No response body
Users
The people at a company who sign in to TUBR to see their forecasts. Invite new users, update their details, or deactivate them when they leave. We take care of roles and onboarding, so all you need to do is add the right people.
List users
GET
/v1/users
The active users of the companies the caller manages: the ones attached to the calling partner, or a company token's own. A partner may narrow to one company with company_code.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | query | string | The company, by code; it must be an active one the caller manages. A partner always names it. A company token or user has its own implied. |
Response 200.
Field | Type | Description |
|---|---|---|
users[].email | string | Required. Up to 500 characters. |
users[].first_name | string or null | Up to 100 characters. |
users[].last_name | string or null | Up to 100 characters. |
users[].role | string | One of admin, user. |
users[].onboarding_stage | integer | One of 1, 2, 3, 4, 5, 6. |
users[].onboarding_stage_status | string | One of in_progress, completed, error. |
Invite a user
POST
/v1/users
Creates a user under a company and emails the sign-in link. The company_code query parameter names the company, which must be one the caller manages; a partner always names it, a company token has its own implied. An inactive user of that company with the same email is reactivated instead, with a 200.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | query | string | The company, by code; it must be an active one the caller manages. A partner always names it. A company token or user has its own implied. |
Request body (application/json).
Field | Type | Description |
|---|---|---|
email | string (email) | Required. |
first_name | string | |
last_name | string | |
phone_num | string | |
role | string | One of admin, user. |
Response 200. Reactivated.
Field | Type | Description |
|---|---|---|
users.email | string | Required. Up to 500 characters. |
users.first_name | string or null | Up to 100 characters. |
users.last_name | string or null | Up to 100 characters. |
users.role | string | One of admin, user. |
users.onboarding_stage | integer | One of 1, 2, 3, 4, 5, 6. |
users.onboarding_stage_status | string | One of in_progress, completed, error. |
Response 201. Created and invited.
Field | Type | Description |
|---|---|---|
users.email | string | Required. Up to 500 characters. |
users.first_name | string or null | Up to 100 characters. |
users.last_name | string or null | Up to 100 characters. |
users.role | string | One of admin, user. |
users.onboarding_stage | integer | One of 1, 2, 3, 4, 5, 6. |
users.onboarding_stage_status | string | One of in_progress, completed, error. |
Update onboarding stage
PATCH
/v1/users
The app's onboarding: advances the stage and status of all the company's users. A partner has none to run and is told user_id is required: it edits one user at /v1/users/{user_id}.
Request body (application/json).
Field | Type | Description |
|---|---|---|
onboarding_stage | integer | |
onboarding_stage_status | string |
Response 200. No body. No response body
Read a user
GET
/v1/users/{user_id}
One user by id, within the companies the caller manages; outside them a 404.
Parameter | In | Type | Description |
|---|---|---|---|
user_id | path | integer | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
users.email | string | Required. Up to 500 characters. |
users.first_name | string or null | Up to 100 characters. |
users.last_name | string or null | Up to 100 characters. |
users.role | string | One of admin, user. |
users.onboarding_stage | integer | One of 1, 2, 3, 4, 5, 6. |
users.onboarding_stage_status | string | One of in_progress, completed, error. |
Update a user
PATCH
/v1/users/{user_id}
Changes the user's first name, last name, email, phone or role. The role is the app's to set: a partner sending one gets a 403. A user cannot change its own role, and a company keeps its last admin.
Parameter | In | Type | Description |
|---|---|---|---|
user_id | path | integer | Required. |
Request body (application/json).
Field | Type | Description |
|---|---|---|
first_name | string | |
last_name | string | |
email | string (email) | |
phone_num | string | |
role | string | One of admin, user. |
Response 200.
Field | Type | Description |
|---|---|---|
users.email | string | Required. Up to 500 characters. |
users.first_name | string or null | Up to 100 characters. |
users.last_name | string or null | Up to 100 characters. |
users.role | string | One of admin, user. |
users.onboarding_stage | integer | One of 1, 2, 3, 4, 5, 6. |
users.onboarding_stage_status | string | One of in_progress, completed, error. |
Deactivate user
DELETE
/v1/users/{user_id}
Marks the user inactive. A company keeps its last admin, and a caller cannot deactivate itself.
Parameter | In | Type | Description |
|---|---|---|---|
user_id | path | integer | Required. |
Response 204. No body. No response body
Outlets
Outlets are the individual venues a company runs, and most of what you send to TUBR is organised by outlet. Add a single site or a whole estate in one request, then view, update or remove outlets as things change. With a partner key you'll see outlets across all your companies; with a company token you'll see just that company's own.
List outlets
GET
/v1/outlets
The outlets the caller may see: a partner sees the ones it supplied across its companies, a company sees its own. company_code narrows the list to one company; a company outside the caller's scope is a 404.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | query | string | Narrow to one company by code; it must be one the caller manages. |
Response 200.
Field | Type | Description |
|---|---|---|
outlets[].currency | string | Required. One of 34 values, for example AED. |
outlets[].timezone | string | Required. One of 597 values, for example Africa/Abidjan. |
outlets[].breakeven | number (double) or null | Required. Weekly sales target (breakeven) for the outlet. |
Create outlets
POST
/v1/outlets
A partner creates one or more outlets under the company named by company_code, which must be attached to it, in one request. The body is a list of at most 100 outlets; they are created together or not at all. Codes are generated unless given, and are unique across TUBR. Opening hours have their own endpoint; prediction feeds are set up automatically.
Parameter | In | Type | Description |
|---|---|---|---|
company_code | query | string | Required. The company to create under, by code; it must be attached to the calling partner. |
Request body (application/json). A JSON array. Each item has these fields.
Field | Type | Description |
|---|---|---|
outlet_name | string | Required. Up to 500 characters. |
address | string | Required. Up to 500 characters. |
outlet_code | string | Generated from the first letters of the name when absent (The Crown: THEC001). Unique across TUBR. Up to 100 characters. |
currency | string | ISO 4217; GBP when absent. Up to 10 characters. |
timezone | string | IANA name; GB when absent. Up to 100 characters. |
breakeven | number (double) or null | |
fixed_costs | number (double) or null | |
variable_costs | number (double) or null |
Response 201.
Field | Type | Description |
|---|---|---|
outlets[].currency | string | Required. One of 34 values, for example AED. |
outlets[].timezone | string | Required. One of 597 values, for example Africa/Abidjan. |
outlets[].breakeven | number (double) or null | Required. Weekly sales target (breakeven) for the outlet. |
Get an outlet
GET
/v1/outlets/{outlet_code}
/v1/outlets/{outlet_code}: read, update or delete one outlet.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
outlets | object | Required. The public shape of an outlet: addressed by code, tied to its company by code. |
outlets.currency | string | Required. One of 34 values, for example AED. |
outlets.timezone | string | Required. One of 597 values, for example Africa/Abidjan. |
outlets.breakeven | number (double) or null | Required. Weekly sales target (breakeven) for the outlet. |
Update an outlet
PATCH
/v1/outlets/{outlet_code}
Partial update: only the fields sent change. The outlet code cannot be changed. Setting is_active to false hides the outlet from a partner's scope.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Request body (application/json).
Field | Type | Description |
|---|---|---|
outlet_name | string | Up to 500 characters. |
address | string | Up to 500 characters. |
currency | string | Up to 10 characters. |
timezone | string | Up to 100 characters. |
breakeven | number (double) or null | |
fixed_costs | number (double) or null | |
variable_costs | number (double) or null | |
is_active | boolean |
Response 200.
Field | Type | Description |
|---|---|---|
outlets | object | Required. The public shape of an outlet: addressed by code, tied to its company by code. |
outlets.currency | string | Required. One of 34 values, for example AED. |
outlets.timezone | string | Required. One of 597 values, for example Africa/Abidjan. |
outlets.breakeven | number (double) or null | Required. Weekly sales target (breakeven) for the outlet. |
Delete an outlet
DELETE
/v1/outlets/{outlet_code}
Soft-deletes the outlet and its prediction feeds.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Response 204. No body. No response body
Opening Hours
Each outlet's regular weekly trading hours. We treat any time outside these hours as closed, so set them before sending orders. You can replace an outlet's schedule from a given date onwards, or set different hours for a fixed period, such as a seasonal change or a refurbishment.
Get opening hours
GET
/v1/outlets/{outlet_code}/opening-hours
Every weekly schedule the outlet has had, oldest first. Each period runs from apply_from to apply_to (null: until further notice) and lists, per weekday, the open-to-close slots in the outlet's timezone. A day not listed is closed.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
opening_hours[].apply_to | string (date-time) or null | Required. Null: until further notice. |
opening_hours[].days | array of objects | Required. Empty means closed throughout; allowed only with apply_to. |
opening_hours[].days[].day | string | Required. One of monday, tuesday, wednesday, thursday, friday, saturday, sunday. |
opening_hours[].days[].slots[].close | string (time) | Required. At or before open means the next day. |
Replace opening hours
PUT
/v1/outlets/{outlet_code}/opening-hours
From apply_from on (now when absent, and it may be in the past) the schedule is exactly the body. The period that straddles apply_from ends there, every period after it is removed, earlier history is kept, and the new schedule holds until further notice. With apply_to the body holds for that window only, a special day or week: the schedule in force resumes after it, and no days means closed throughout. Times are wall-clock in the outlet's timezone; a close at or before its open means the next day. Answers with the same body as GET.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Request body (application/json).
Field | Type | Description |
|---|---|---|
apply_from | string (date-time) | ISO 8601; without an offset it is the outlet's local time. Now when absent; may be in the past. |
apply_to | string (date-time) or null | ISO 8601; without an offset it is the outlet's local time. Given, the body holds until then and the previous schedule resumes. |
days | array of objects | Required. Empty means closed throughout; allowed only with apply_to. |
days[].day | string | Required. One of monday, tuesday, wednesday, thursday, friday, saturday, sunday. |
days[].slots | array of objects | Required. |
days[].slots[].open | string (time) | Required. |
days[].slots[].close | string (time) | Required. At or before open means the next day. |
Response 200.
Field | Type | Description |
|---|---|---|
opening_hours[].apply_to | string (date-time) or null | Required. Null: until further notice. |
opening_hours[].days | array of objects | Required. Empty means closed throughout; allowed only with apply_to. |
opening_hours[].days[].day | string | Required. One of monday, tuesday, wednesday, thursday, friday, saturday, sunday. |
opening_hours[].days[].slots[].close | string (time) | Required. At or before open means the next day. |
Products
Each outlet's product catalogue and the categories its products belong to. We match incoming orders against this catalogue, so keeping it complete and up to date helps us produce more accurate forecasts.
Get product categories
GET
/v1/outlets/{outlet_code}/categories
The outlet's product categories. An outlet with none answers an empty list.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
categories[].external_category_id | string | Required. Up to 100 characters. |
categories[].category_name | string | Required. Up to 100 characters. |
Get a product category
GET
/v1/outlets/{outlet_code}/categories/{external_category_id}
One category by external_category_id; an unknown id is a 404.
Parameter | In | Type | Description |
|---|---|---|---|
external_category_id | path | string | Required. |
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
categories.external_category_id | string | Required. Up to 100 characters. |
categories.category_name | string | Required. Up to 100 characters. |
List products
GET
/v1/outlets/{outlet_code}/products
The outlet's products. An outlet with none answers an empty list.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
products[].external_product_id | string | Required. Up to 100 characters. |
products[].product_name | string | Required. Up to 200 characters. |
products[].external_category_id | string | Required. Up to 100 characters. |
products[].category_name | string | Required. Up to 100 characters. |
Create or update products
PUT
/v1/outlets/{outlet_code}/products
Create or update the outlet's products, matched by external_product_id. The body is a list. The work is queued, so the answer is a 202 with a task id; a GET shows the result once it has run.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Request body (application/json). A JSON array. Each item has these fields.
Field | Type | Description |
|---|---|---|
external_product_id | string | Required. Up to 100 characters. |
product_name | string | Required. Up to 200 characters. |
external_category_id | string | Required. Up to 100 characters. |
category_name | string | Required. Up to 100 characters. |
sales_price | number (double) | Required. |
cost_price | number (double) or null | |
stock_quantity | integer or null |
Response 202.
Get a product
GET
/v1/outlets/{outlet_code}/products/{external_product_id}
One product by external_product_id; an unknown id is a 404.
Parameter | In | Type | Description |
|---|---|---|---|
external_product_id | path | string | Required. |
outlet_code | path | string | Required. |
Response 200.
Field | Type | Description |
|---|---|---|
products | object | Required. One product, as PUT takes it in a list and GET returns it in a list. |
products.external_product_id | string | Required. Up to 100 characters. |
products.product_name | string | Required. Up to 200 characters. |
products.external_category_id | string | Required. Up to 100 characters. |
products.category_name | string | Required. Up to 100 characters. |
Delete a product
DELETE
/v1/outlets/{outlet_code}/products/{external_product_id}
Soft-deletes one product by external_product_id.
Parameter | In | Type | Description |
|---|---|---|---|
external_product_id | path | string | Required. |
outlet_code | path | string | Required. |
Response 204. No body. No response body
Orders
The sales data that powers your forecasts. Send order lines as they happen, or upload your sales history in bulk to give our models a head start. Orders can be sent as JSON or CSV, and you can read them back page by page.
List orders
GET
/v1/outlets/{outlet_code}/orders
A page of the outlet's order lines, with optional date filtering in the outlet's timezone.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
end_date | query | string (date-time) | End of date range (ISO 8601); needs start_date. |
page | query | integer | Page number (1-indexed). |
page_size | query | integer | Items per page (max 1000). |
sort_by | query | string | Sort field ('id' or 'timestamp'). |
sort_dir | query | string | Sort direction ('asc' or 'desc'). |
start_date | query | string (date-time) | Start of date range (ISO 8601); needs end_date. |
Response 200.
Field | Type | Description |
|---|---|---|
results[].transaction_code | string or null | Up to 100 characters. |
results[].line_order_uid | string or null | Up to 100 characters. |
Upload orders
POST
/v1/outlets/{outlet_code}/orders
Upload order lines for the outlet in the path, either as a JSON list or as a CSV file in a multipart field named csv_file. The file starts with a header row naming the same six columns, in any order: timestamp, transaction_code, line_order_uid, external_product_id, product_quantity, total_sales. Other columns are ignored. Rows carry no outlet_code, the path names it. The work is queued, so the answer is a 202 with a task id.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
Request body, JSON (application/json). A JSON array. Each item has these fields.
Field | Type | Description |
|---|---|---|
timestamp | string (date-time) | Required. |
transaction_code | string | Required. |
line_order_uid | string | Required. |
external_product_id | string | Required. |
product_quantity | number (double) | Required. |
total_sales | number (double) | Required. |
Request body, file upload (multipart/form-data).
Field | Type | Description |
|---|---|---|
csv_file | string (uri) | Required. |
Response 202.
Predictions
Sales forecasts for each outlet, available at daily or hourly resolution and broken down by feed type. Your first forecast is ready about a day after the first orders arrive, and it gets sharper as more data comes in.
Get predictions
GET
/v1/outlets/{outlet_code}/predictions
The outlet's predictions, keyed by feed type, at one resolution (daily unless asked otherwise). Without a window the answer is the latest forecast: from now, rounded down to the resolution in the outlet's timezone, as far ahead as the feed predicts. With start_datetime and end_datetime (ISO 8601 UTC, end exclusive) the answer is that window, which may reach back one year and forward to the end of the forecast horizon. Pages are cut by timestamp, so every feed type on a page covers the same instants. Only the latest forecast for each timestamp is returned.
Parameter | In | Type | Description |
|---|---|---|---|
outlet_code | path | string | Required. |
end_datetime | query | string (date-time) | End of the window (ISO 8601 UTC, exclusive); needs start_datetime. |
feed_type | query | string | One feed type; all when absent. One of number_of_transaction, total_sales. |
page | query | integer | 1-indexed. |
page_size | query | integer | Timestamps per page (max 1000). |
resolution | query | string | hourly or daily; daily when absent. One of daily, hourly. |
sort_dir | query | string | Timestamp order, 'asc' (default) or 'desc'. |
start_datetime | query | string (date-time) | Start of the window (ISO 8601 UTC); needs end_datetime. |
Response 200.
Built from backend e7a6d5a8dd.