05-19-2026 02:49 PM - edited 05-19-2026 03:51 PM
If you've built Business Views in Incorta, you already know how powerful they are as a semantic layer — joins, calculations, and row-level security all baked in. But at some point you'll want to pull that curated data into a Python script: a downstream pipeline, a Streamlit dashboard, a pandas analysis, or a scheduled export.
Incorta exposes a REST API that lets you do exactly that. This post walks through the query pattern, the pagination gotcha that will bite you on large views, and a reusable function you can drop into any project.
To check for the API payload, on your browser login to incorta first and then go to <incorta host>/incorta/api
Incorta supports Personal Access Tokens as the preferred authentication method for API access. No session handshake, no cookie management — just a static Bearer token in the request header.
You generate a PAT from your Incorta user profile settings. Once you have it, every request is authenticated with a single header:
Keep the PAT in an environment variable or a secrets manager — never hard-code it in your script.
The endpoint for querying Business Views is:
POST /incorta/api/v2/{tenant}/query
Note the path structure: /incorta/api/v2/ (not /incorta-apis/), followed by your tenant name. If your Incorta instance has multiple tenants, this is where you target the right one — typically default for single-tenant deployments.
This is the part that surprises most people coming from a SQL-first background. The Incorta query API is not a raw SQL endpoint. Instead, you specify the columns you want as a list of measures, each referencing a fully qualified field path in Schema.View.Column format:
The label is what Incorta uses as the column header in the response. The field is the fully qualified reference to the Business View column.
The full payload looks like this:
payload = {
"stringify": False, # return native types, not strings
"query": {
"format": "json",
"aggregate": False, # raw row-level data, no grouping
"startRow": 0,
"pageSize": 50000,
"measures": measures,
}
}
Two flags worth understanding:
stringify: False — tells Incorta to return numbers as numbers and dates as dates rather than coercing everything to strings. Even with this set, you may still get string values for some column types, so defensive type casting on the Python side is still good practice.aggregate: False — disables automatic aggregation. Without this, Incorta may roll up numeric columns, which is not what you want when pulling row-level data.The API does not return all rows in one shot. It uses a startRow / pageSize cursor pattern. If your Business View has 200,000 rows and you request pageSize: 50000, you'll get four pages. You detect the end of data when the returned page has fewer rows than pageSize:
The response data field is a list of dicts (one per row). Incorta uses the label values you specified in your measures list as the dict keys — but be aware that it returns the display labels rather than the original field names in some configurations. The safest approach is to rename columns positionally after loading:
Even with stringify: False, numeric and date columns often come back as strings. A quick post-processing pass handles this:
Using errors="coerce" means bad values become NaN rather than raising an exception — important when you're dealing with real-world EBS data that occasionally has nulls or unexpected formats in numeric fields.
Here is the complete python script:
"""
Incorta Business View Query Script
Queries Sales_BS.Orders using the Incorta measures-based Query REST API.
Authentication via Personal Access Token (PAT).
Usage:
1. Set INCORTA_SERVER, INCORTA_TENANT, and PAT below
2. Update COLUMNS to match your Sales_BS.Orders business view
3. Run: python incorta_query.py
"""
import requests
import json
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
# ─────────────────────────────────────────────
# Configuration — update these for your env
# ─────────────────────────────────────────────
INCORTA_SERVER = "https://<your-incorta-host>" # e.g. https://mycompany.incorta.com
INCORTA_TENANT = "<your-tenant>" # e.g. "default"
PAT = "<your-personal-access-token>" # Personal Access Token
BS_SCHEMA = "Sales_BS"
VIEW_NAME = "Orders"
PAGE_SIZE = 50000
# Update this list to match the actual column names in your Sales_BS.Orders business view.
# Names must match the technical field names in the schema XML (not display labels).
COLUMNS = [
"ORDER_NUMBER",
"ORDER_DATE",
"CUSTOMER_NAME",
"ITEM_NUMBER",
"ITEM_DESCRIPTION",
"ORDERED_QUANTITY",
"UNIT_SELLING_PRICE",
"ORDER_VALUE",
"ORDER_STATUS",
"ORGANIZATION_CODE",
]
# ─────────────────────────────────────────────
# Build request headers
# ─────────────────────────────────────────────
def get_headers() -> dict:
return {
"Accept": "application/json",
"Authorization": f"Bearer {PAT}",
"Content-Type": "application/json",
}
# ─────────────────────────────────────────────
# Query a business view with pagination
# ─────────────────────────────────────────────
def query_incorta_view(
schema: str,
view: str,
columns: list,
page_size: int = PAGE_SIZE,
) -> pd.DataFrame:
"""
Query an Incorta Business View via the measures-based REST API.
Paginates automatically using startRow / pageSize.
Returns a pandas DataFrame with lowercase column names.
API endpoint : POST /incorta/api/v2/{tenant}/query
Auth : Bearer PAT in Authorization header
Payload type : measures-based (not raw SQL)
"""
url = f"{INCORTA_SERVER}/incorta/api/v2/{INCORTA_TENANT}/query"
headers = get_headers()
# Each column is a measure referencing Schema.View.Column
measures = [
{"label": col, "field": f"{schema}.{view}.{col}"}
for col in columns
]
all_rows = []
start_row = 0
while True:
payload = json.dumps({
"stringify": False, # return native types, not everything as strings
"query": {
"format": "json",
"aggregate": False, # row-level data — no auto-aggregation
"startRow": start_row,
"pageSize": page_size,
"measures": measures,
}
})
print(f" Fetching rows {start_row:,} – {start_row + page_size:,} ...")
try:
response = requests.post(
url=url, headers=headers, data=payload, timeout=120
)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"❌ HTTP error: {e}")
print(f" Response: {response.text[:300]}")
break
except requests.exceptions.RequestException as e:
print(f"❌ Request failed: {e}")
break
result = response.json()
page = result.get("data", [])
if not page:
break
all_rows.extend(page)
print(f" → {len(page):,} rows received (total so far: {len(all_rows):,})")
if len(page) < page_size: # last page — no more to fetch
break
start_row += page_size
# ── Build DataFrame ────────────────────────
if not all_rows:
print(f"⚠️ No data returned for {schema}.{view}")
return pd.DataFrame(columns=[c.lower() for c in columns])
df = pd.DataFrame(all_rows)
# Incorta may return display labels rather than field names — rename positionally
if list(df.columns) != columns and len(df.columns) == len(columns):
df.columns = columns
# Lowercase all column names for consistency
df.columns = [c.lower() for c in df.columns]
return df
# ─────────────────────────────────────────────
# Type casting
# ─────────────────────────────────────────────
def cast_types(df: pd.DataFrame) -> pd.DataFrame:
"""
Cast columns to appropriate Python types.
Even with stringify=False, Incorta often returns numbers and dates as strings.
errors='coerce' turns unparseable values into NaN rather than raising.
"""
date_cols = ["order_date"]
numeric_cols = ["ordered_quantity", "unit_selling_price", "order_value"]
for col in date_cols:
if col in df.columns:
df[col] = pd.to_datetime(df[col], errors="coerce")
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
# ─────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────
def main():
print(f"\n🔍 Querying {BS_SCHEMA}.{VIEW_NAME} ...")
print(f" Server : {INCORTA_SERVER}")
print(f" Tenant : {INCORTA_TENANT}")
print(f" Columns: {len(COLUMNS)}")
print(f" Page : {PAGE_SIZE:,} rows\n")
df = query_incorta_view(BS_SCHEMA, VIEW_NAME, COLUMNS, page_size=PAGE_SIZE)
if df.empty:
print("No data to process. Exiting.")
return
df = cast_types(df)
# ── Summary ────────────────────────────────
print(f"\n✅ Done — {len(df):,} rows × {len(df.columns)} columns")
print(f"\nColumn dtypes:\n{df.dtypes.to_string()}")
print(f"\nFirst 10 rows:\n")
print(df.head(10).to_string(index=False))
# ── Save to CSV ────────────────────────────
output_file = "sales_bs_orders.csv"
df.to_csv(output_file, index=False)
print(f"\n💾 Saved to {output_file}")
return df
if __name__ == "__main__":
main()If you don't know the exact column names in a Business View, the easiest way to find them is to open the schema XML from the Incorta content manager, or use the Incorta UI to inspect the Business View definition. Column names in the API payload must match the technical field names defined in the schema, not the display labels shown in dashboards.
The Incorta measures-based REST API is a clean, stable way to extract Business View data into Python. The key points to remember:
Authorization: Bearer/incorta/api/v2/{tenant}/query — not a SQL endpoint{"label": ..., "field": "Schema.View.Column"} measuresaggregate: False for row-level datastartRow / pageSizeOnce this pattern is in place, Incorta Business Views become a first-class data source for any Python-based analytics workflow — pipelines, dashboards, exports, or ad-hoc analysis.