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:
bash
curl https://api.prod.tubr.live/v1/companies \ -H "Authorization: Bearer <your-token>"
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.
Request · bash
curl "https://api.prod.tubr.live/v1/companies" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
Response 200.
Response 200 · json
[ { "company_code": "string", "company_name": "string", "phone": "string", "active": true, "created_at": "2026-01-01T09:00:00Z" } ]

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.
Request · bash
curl -X POST "https://api.prod.tubr.live/v1/companies" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "company_name": "string", "phone": "string" }'
Response 201.
Response 201 · json
{ "company_code": "string", "company_name": "string", "phone": "string", "active": true, "created_at": "2026-01-01T09:00:00Z" }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/companies/$COMPANY_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
Response 200.
Response 200 · json
{ "company_code": "string", "company_name": "string", "phone": "string", "active": true, "created_at": "2026-01-01T09:00:00Z" }

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.
Request · bash
curl -X DELETE "https://api.prod.tubr.live/v1/companies/$COMPANY_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Request · bash
curl "https://api.prod.tubr.live/v1/users" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "users": [ { "email": "string", "first_name": "string", "last_name": "string", "role": "admin", "id": 0, "company_id": 0, "onboarding_stage": 1, "onboarding_stage_status": "in_progress", "company_code": "string", "phone_num": "string" } ] }

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.
Request · bash
curl -X POST "https://api.prod.tubr.live/v1/users" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "name@example.com", "first_name": "string", "last_name": "string", "phone_num": "string", "role": "admin" }'
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 200 · json
{ "users": { "email": "string", "first_name": "string", "last_name": "string", "role": "admin", "id": 0, "company_id": 0, "onboarding_stage": 1, "onboarding_stage_status": "in_progress", "company_code": "string", "phone_num": "string" } }
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.
Response 201 · json
{ "users": { "email": "string", "first_name": "string", "last_name": "string", "role": "admin", "id": 0, "company_id": 0, "onboarding_stage": 1, "onboarding_stage_status": "in_progress", "company_code": "string", "phone_num": "string" } }

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
Request · bash
curl -X PATCH "https://api.prod.tubr.live/v1/users" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "onboarding_stage": 0, "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.
Request · bash
curl "https://api.prod.tubr.live/v1/users/$USER_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "users": { "email": "string", "first_name": "string", "last_name": "string", "role": "admin", "id": 0, "company_id": 0, "onboarding_stage": 1, "onboarding_stage_status": "in_progress", "company_code": "string", "phone_num": "string" } }

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.
Request · bash
curl -X PATCH "https://api.prod.tubr.live/v1/users/$USER_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "string", "last_name": "string", "email": "name@example.com", "phone_num": "string", "role": "admin" }'
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.
Response 200 · json
{ "users": { "email": "string", "first_name": "string", "last_name": "string", "role": "admin", "id": 0, "company_id": 0, "onboarding_stage": 1, "onboarding_stage_status": "in_progress", "company_code": "string", "phone_num": "string" } }

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.
Request · bash
curl -X DELETE "https://api.prod.tubr.live/v1/users/$USER_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "outlets": [ { "outlet_code": "string", "company_code": "string", "outlet_name": "string", "address": "string", "currency": "AED", "timezone": "Africa/Abidjan", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0, "is_active": true } ] }

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
Request · bash
curl -X POST "https://api.prod.tubr.live/v1/outlets?company_code=$COMPANY_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '[ { "outlet_name": "string", "address": "string", "outlet_code": "string", "currency": "string", "timezone": "string", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0 } ]'
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.
Response 201 · json
{ "outlets": [ { "outlet_code": "string", "company_code": "string", "outlet_name": "string", "address": "string", "currency": "AED", "timezone": "Africa/Abidjan", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0, "is_active": true } ] }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "outlets": { "outlet_code": "string", "company_code": "string", "outlet_name": "string", "address": "string", "currency": "AED", "timezone": "Africa/Abidjan", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0, "is_active": true } }

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
Request · bash
curl -X PATCH "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "outlet_name": "string", "address": "string", "currency": "string", "timezone": "string", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0, "is_active": true }'
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.
Response 200 · json
{ "outlets": { "outlet_code": "string", "company_code": "string", "outlet_name": "string", "address": "string", "currency": "AED", "timezone": "Africa/Abidjan", "breakeven": 0, "fixed_costs": 0, "variable_costs": 0, "is_active": true } }

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.
Request · bash
curl -X DELETE "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/opening-hours" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "opening_hours": [ { "apply_from": "2026-01-01T09:00:00Z", "apply_to": "2026-01-01T09:00:00Z", "days": [ { "day": "monday", "slots": [ { "open": "09:00:00", "close": "09:00:00" } ] } ], "timezone": "string" } ] }

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.
Request · bash
curl -X PUT "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/opening-hours" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "apply_from": "2026-01-01T09:00:00Z", "apply_to": "2026-01-01T09:00:00Z", "days": [ { "day": "monday", "slots": [ { "open": "09:00:00", "close": "09:00:00" } ] } ] }'
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.
Response 200 · json
{ "opening_hours": [ { "apply_from": "2026-01-01T09:00:00Z", "apply_to": "2026-01-01T09:00:00Z", "days": [ { "day": "monday", "slots": [ { "open": "09:00:00", "close": "09:00:00" } ] } ], "timezone": "string" } ] }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/categories" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
Response 200.
Field
Type
Description
categories[].external_category_id
string
Required. Up to 100 characters.
categories[].category_name
string
Required. Up to 100 characters.
Response 200 · json
{ "categories": [ { "external_category_id": "string", "category_name": "string" } ] }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/categories/$EXTERNAL_CATEGORY_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
Response 200.
Field
Type
Description
categories.external_category_id
string
Required. Up to 100 characters.
categories.category_name
string
Required. Up to 100 characters.
Response 200 · json
{ "categories": { "external_category_id": "string", "category_name": "string" } }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/products" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "products": [ { "external_product_id": "string", "product_name": "string", "external_category_id": "string", "category_name": "string", "sales_price": 0, "cost_price": 0, "stock_quantity": 0 } ] }

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
Request · bash
curl -X PUT "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/products" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '[ { "external_product_id": "string", "product_name": "string", "external_category_id": "string", "category_name": "string", "sales_price": 0, "cost_price": 0, "stock_quantity": 0 } ]'
Response 202.
Response 202 · json
{ "task_id": "string" }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/products/$EXTERNAL_PRODUCT_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "products": { "external_product_id": "string", "product_name": "string", "external_category_id": "string", "category_name": "string", "sales_price": 0, "cost_price": 0, "stock_quantity": 0 } }

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.
Request · bash
curl -X DELETE "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/products/$EXTERNAL_PRODUCT_ID" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/orders" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
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.
Response 200 · json
{ "pagination": { "page": 0, "page_size": 0, "total_pages": 0, "total_count": 0, "has_next": true, "has_previous": true }, "results": [ { "id": 0, "product_name": "string", "category_name": "string", "timestamp": "2026-01-01T09:00:00Z", "transaction_code": "string", "line_order_uid": "string", "total_sales": 0, "product_quantity": 0, "outlet": 0, "order_mode": 0, "product_category": 0, "product": 0 } ] }

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.
Request, application/json · bash
curl -X POST "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/orders" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -H "Content-Type: application/json" \ -d '[ { "timestamp": "2026-01-01T09:00:00Z", "transaction_code": "string", "line_order_uid": "string", "external_product_id": "string", "product_quantity": 0, "total_sales": 0 } ]'
Request, multipart/form-data · bash
curl -X POST "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/orders" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY" \ -F "csv_file=@./file.csv"
Response 202.
Response 202 · json
{ "task_id": "string" }

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.
Request · bash
curl "https://api.prod.tubr.live/v1/outlets/$OUTLET_CODE/predictions" \ -H "Authorization: Bearer $TUBR_PARTNER_KEY"
Response 200.
Response 200 · json
{ "pagination": { "page": 0, "page_size": 0, "total_pages": 0, "total_count": 0, "has_next": true, "has_previous": true }, "outlet_code": "string", "resolution": "string", "start_datetime": "2026-01-01T09:00:00Z", "end_datetime": "2026-01-01T09:00:00Z", "predictions": { "total_sales": [ { "timestamp": "2026-01-01T09:00:00Z", "value": 0, "lower_bound": 0, "upper_bound": 0 } ], "number_of_transaction": [ { "timestamp": "2026-01-01T09:00:00Z", "value": 0, "lower_bound": 0, "upper_bound": 0 } ] } }
Built from backend e7a6d5a8dd.