Millbase API
Build with your organization’s live data.
Bring projects, inventory, labor, and costs into your own apps and workflows with a straightforward, read-only REST API.
https://app.millbase.ca/api/v1Your first request
You’ll need a Millbase account and administrator access to the organization you want to connect.
Create an API key
In Millbase, open Settings → API and integrations → API keys. Name your integration, select Warehouse and projects, choose an expiry, then generate a key.
Open API settingsSave your key
Copy it when it’s created; the full key is shown only once. Replace the placeholder below in your terminal. Store production keys in your server’s secret manager.
Terminalexport MILLBASE_API_KEY='YOUR_MILLBASE_API_KEY'Read your projects
Run this on your server or in a terminal. Node.js 22+ and Python 3 examples use built-in libraries.
curl --fail-with-body --get 'https://app.millbase.ca/api/v1/data/projects' \ -H "Authorization: Bearer $MILLBASE_API_KEY" \ --data-urlencode 'fields=id,name,status' \ --data-urlencode 'limit=10'const key = process.env.MILLBASE_API_KEY; if (!key) throw new Error('Set MILLBASE_API_KEY first'); const url = new URL('https://app.millbase.ca/api/v1/data/projects'); url.search = new URLSearchParams({ fields: 'id,name,status', limit: '10', }).toString(); const response = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, signal: AbortSignal.timeout(30_000), }); if (!response.ok) throw new Error(await response.text()); const { data, pagination } = await response.json(); console.log(data, pagination);import json import os from urllib.parse import urlencode from urllib.request import Request, urlopen params = urlencode({"fields": "id,name,status", "limit": 10}) request = Request( f"https://app.millbase.ca/api/v1/data/projects?{params}", headers={ "Authorization": f"Bearer {os.environ['MILLBASE_API_KEY']}" }, ) with urlopen(request, timeout=30) as response: result = json.load(response) print(result["data"], result["pagination"])
What comes back
A JSON object with your rows and pagination details. This is a fictional example; your response contains your organization’s records. An empty data array means no records matched.
{
"data": [
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example kitchen",
"status": "active"
}
],
"pagination": {
"limit": 10,
"offset": 0,
"nextOffset": null
}
}Authentication
Send your key in the Authorization header with every request: Authorization: Bearer YOUR_MILLBASE_API_KEY.
Use one key per integration, with only the permissions it needs. Keys can expire after 30, 90, or 365 days. Revoke a key in API settings to stop access, then create a replacement when rotating credentials.
Keep keys in server-side environment variables or a secret manager. Never put them in browser code, public repositories, URLs, or chat messages.
Endpoint reference
Start with the catalog to see exactly which datasets and fields your key can access.
curl --fail-with-body 'https://app.millbase.ca/api/v1/resources' \
-H "Authorization: Bearer $MILLBASE_API_KEY"/resourcesDiscover allowed datasets and fields/data/{resource}Read rows with an optional equality filter/queryRead rows with multiple filtersAll paths below are relative to the base URL. This reference lists every public dataset; your key’s permissions determine which ones you can read. Expand a row’s fields to see its allowed columns.
25 / 25 endpoints
| Endpoint | Required permission | Fields |
|---|---|---|
GET/data/organization | operations:read | 14 fieldsidnameslugtimezonecurrencylanguagelogo_urlaccent_colorweek_starts_ondaily_overtime_minutesweekly_overtime_minutesovertime_multipliercreated_atupdated_at |
GET/data/projects | operations:read | 13 fieldsidnamenumberclientdescriptionstatusprogress_stageparent_project_idtag_idsstart_datelabor_hours_estimatecreated_atupdated_at |
GET/data/project-estimates | financials:read | 8 fieldsidnameestimated_delivery_costestimated_installation_costestimated_labor_costestimated_materials_costestimated_others_costupdated_at |
GET/data/project-clients | people:read | 9 fieldsproject_idcontact_nameaddressemailphonewebsitenotesfollow_up_dateupdated_at |
GET/data/project-categories | operations:read | 5 fieldsproject_idcodelabelis_customsort_order |
GET/data/project-tags | operations:read | 4 fieldsidnamecolorcreated_at |
GET/data/materials | operations:read | 10 fieldsidcodenameuniton_handminimum_stockactiveimage_urlcreated_atupdated_at |
GET/data/material-costs | financials:read | 5 fieldsidcodenameunit_costupdated_at |
GET/data/material-lots | financials:read | 7 fieldsidmaterial_idquantity_receivedquantity_remainingunit_costcreated_atupdated_at |
GET/data/project-materials | financials:read | 11 fieldsidproject_idmaterial_idcustom_namequantityremaining_quantityunitunit_costtotal_costcreated_atupdated_at |
GET/data/inventory-activity | financials:read | 14 fieldsidmaterial_idproject_idproject_nametask_log_idactivity_typequantityquantity_deltabalance_afterunit_costfrom_locationto_locationnotecreated_at |
GET/data/workstations | operations:read | 6 fieldsidcodenamelocationcreated_atupdated_at |
GET/data/workers | people:read | 11 fieldsidnameemailroleworker_typearchivedstation_codeactive_log_idavatar_urlcreated_atupdated_at |
GET/data/worker-compensation | people:readfinancials:read | 4 fieldsidnamehourly_rateupdated_at |
GET/data/members | people:read | 7 fieldsidnameemailroleavatar_urlcreated_atupdated_at |
GET/data/task-logs | operations:read | 11 fieldsidproject_idworker_idworkstation_idcategory_codestart_atend_atsecondsstatuscreated_atupdated_at |
GET/data/task-segments | operations:read | 6 fieldsidtask_log_idstart_atend_atsecondscreated_at |
GET/data/task-materials | operations:read | 7 fieldstask_log_idmaterial_idproject_material_idcodenamequantityunit |
GET/data/historical-labor-costs | financials:read | 4 fieldstask_log_idlabor_costsourcecaptured_at |
GET/data/shifts | financials:read | 8 fieldsidstart_atend_atstatustotal_secondstotal_earningscreated_atupdated_at |
GET/data/time-adjustments | people:read | 7 fieldsidworker_iddatereasonsecondscreated_atupdated_at |
GET/data/project-files | operations:read | 9 fieldsidproject_idfolder_idnamecontent_typesizestatuscreated_atupdated_at |
GET/data/project-folders | operations:read | 5 fieldsidproject_idnamecreated_atupdated_at |
GET/data/designs | designs:read | 7 fieldsidproject_idnamesharedversioncreated_atupdated_at |
GET/data/design-assets | designs:read | 6 fieldsiddesign_idnamesizestatuscreated_at |
Generated from Millbase’s public OpenAPI schema. View live OpenAPI · Download this reference
Filters & queries
For a simple GET, use fields, limit, and offset. Add filter=id and value=YOUR_RECORD_UUID to find one record. URL-encode parameter values.
Use POST /query for several filters or date ranges. For example, read task logs for a project. Replace PROJECT_UUID with an ID from your projects response.
curl --fail-with-body 'https://app.millbase.ca/api/v1/query' \
-H "Authorization: Bearer $MILLBASE_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"resource": "task-logs",
"fields": [
"id",
"project_id",
"worker_id",
"seconds",
"status"
],
"filters": [
{
"field": "project_id",
"op": "eq",
"value": "PROJECT_UUID"
}
],
"limit": 50,
"offset": 0
}'{
"resource": "task-logs",
"fields": [
"id",
"project_id",
"worker_id",
"seconds",
"status"
],
"filters": [
{
"field": "project_id",
"op": "eq",
"value": "PROJECT_UUID"
}
],
"limit": 50,
"offset": 0
}| Parameter | How to use it |
|---|---|
resource | Required. A dataset name from the catalog, such as projects. |
fields | Optional. Select 1–40 allowed fields. Omit to return all public fields for the resource. |
filters | Up to 10 filters. All must match (AND). Supported operators: eq (equals), gte (at least), lte (at most). Use ISO 8601 timestamps for date ranges. |
limit | 1–100 rows. Default: 50. |
offset | 0–1,000,000. Default: 0. Use nextOffset from the previous page. |
Queries accept only the catalog’s named fields. Wildcards, joins, SQL, arbitrary tables, and organization overrides are not supported.
Pagination
Read pagination.nextOffset and pass it as offset in the next request. Stop when it is null. Keep the same resource, fields, and filters across pages.
import { setTimeout } from 'node:timers/promises';
let offset = 0;
do {
const response = await fetch('https://app.millbase.ca/api/v1/query', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MILLBASE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ resource: 'materials', limit: 100, offset }),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(await response.text());
const { data, pagination } = await response.json();
console.log(data);
offset = pagination.nextOffset;
if (offset !== null) await setTimeout(1100);
} while (offset !== null);Each page reads current stored data without response caching. Records may change while you paginate; this is not a frozen export. For ongoing work, elapsed seconds reflect the last saved value, so use start and end timestamps when calculating running durations.
Permissions & privacy
API and MCP access is available on Free, Pro, and Enterprise. An organization administrator chooses the permissions for each integration.
| Permission | What it covers |
|---|---|
operations:readWarehouse and projects | Projects, stock levels, stations, and task logs. |
people:readWorkers and client contacts | Worker details, members, and project contacts. |
financials:readCosts and financial data | Project estimates, material costs, shifts, and historical labor costs. |
designs:readDesigns and design assets | Design and asset metadata. |
Worker compensation requires both people:read and financials:read. The endpoint reference lists every required permission; all listed permissions must be granted.
Limits & errors
API and MCP share these organization limits. Creating more keys does not increase them. Authenticated discovery, initialization, and invalid queries also count. Requests are limited to 16 KB and responses to 1 MB.
| HTTP | What to do |
|---|---|
400 | Check the resource, field names, filter types, and pagination values. |
401 | Check the Bearer header. Your key may be invalid, expired, or revoked. |
403 | Check your key’s permissions and administrator access to the organization. |
413 | Send a smaller request, select fewer fields, or reduce the page size. |
429 | Wait for the Retry-After interval before retrying. Check shared organization usage. |
503 | The data is temporarily unavailable. Retry with a delay. |
Errors return error.code and error.message. For 429 responses, respect the Retry-After header; use backoff with jitter for temporary failures. The X-Monthly-Remaining header reports remaining organization requests.
Connecting an agent instead of building an app? Follow the MCP guide.