cancel
Showing results for 
Search instead for 
Did you mean: 
amit_kothari
Employee
Employee

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


Authentication: Personal Access Token (PAT)

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 Query Endpoint

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.


The Payload: Measures, Not SQL

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:

 
python
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.

Pagination

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:


Parsing the Response into a DataFrame

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:


Type Casting

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.


Putting It All Together

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()
 
The same function works for any Business View in any schema — swap the schema, view, and column list and you're done. This makes it straightforward to build multi-view pipelines, such as loading four or five views in sequence to feed a Streamlit dashboard or a downstream data model.

A Note on Column Discovery

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.


Summary

The Incorta measures-based REST API is a clean, stable way to extract Business View data into Python. The key points to remember:

  • Authenticate with a PAT via Authorization: Bearer
  • Target /incorta/api/v2/{tenant}/query — not a SQL endpoint
  • Specify columns as {"label": ..., "field": "Schema.View.Column"} measures
  • Set aggregate: False for row-level data
  • Always paginate with startRow / pageSize
  • Cast types defensively after loading into a DataFrame

Once 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.

Link to API Doc

Best Practices Index
Best Practices

Just here to browse knowledge? This might help!

Contributors
Version history
Last update:
‎05-19-2026 03:51 PM
Updated by: