Rundocs › Integrations
External Data API
Last updated 2026-10-13
The External Data API is a read-only JSON interface for pulling your Rundoo data into BI tools, spreadsheets, or your own backends. Designed around self-discoverability so clients can list datasets, inspect their columns, and query exactly the shape they want.
It mirrors the Report Builder: the same datasets, the same columns, the same shaping. Anything you can pull as a custom report, you can pull through the API, so the simplest way to judge "can it do X" is to check whether the Report Builder can do X. Access is currently free.
API access is provisioned by Rundoo Support. Email support@rundoo.ai with the company subdomain and what you're building, and we'll issue a bearer token. There is no in-product API-token page. Tokens live with Support.
Authentication
Every request needs a bearer token. Unauthenticated requests get 401 Unauthorized; authenticated users without sufficient permissions get 403 Forbidden.
Authorization: Bearer rdk_abcdwkyz12345678
Tokens are scoped to one company subdomain. Treat them like passwords. Anything with a token can read every dataset listed below, including sales, costs, and customer details.
Base URL
https://eapi.rundoo.app/v1/{subdomain}/datasets
{subdomain} is your Rundoo subdomain, the part before .rundoo.app in your POS URL. If your POS is at acme.rundoo.app, your subdomain is acme.
Concepts
Datasets
A dataset is a queryable table of business data scoped to one domain. products is the product catalog; sold_products is sale line-item data (the primary dataset for revenue, profit, and quantity analytics).
The full list lives behind List datasets so it's always current. Today's datasets:
| Dataset | Description |
|---|---|
products |
Product catalog |
customers |
Customer records |
sold_products |
Sale line items (primary dataset for revenue, profit, quantity) |
sales |
Sale-level data including tax and totals |
order_products |
Purchase order line items |
shipment_products |
Order shipment line items |
transfer_products |
Inventory transfer line items |
inventory |
Current inventory levels and valuation |
inventory_counts |
Physical inventory count records |
product_costs |
Product cost records |
vendors |
Vendor records |
jobs |
Job records |
interactions |
Customer interaction records |
tags |
Tag records |
staff |
Staff records |
price_rules |
Pricing rule configuration |
sold_product_pricing_tiers |
Pricing tier details for sold products |
spiffs |
Spiff (sales incentive) records |
gift_card_balances |
Gift card balances |
transactions |
Ledger-level transactions |
taxable_transactions |
Tax-relevant transaction details |
tax_rate |
Tax rate configuration |
aging_by_transaction |
Accounts receivable aging detail |
Columns: labels vs metrics
Every dataset exposes a set of columns identified by lowercase, underscore-separated names (e.g. product_name, sold_product_revenue_net). Columns fall into two categories:
-
Labels are descriptive or categorical (
product,customer_name,location,date). Including label columns in a query automatically groups rows by every unique combination of those values. -
Metrics are numeric or aggregate (
sold_product_revenue_net,sale_total,inventory_on_hand). Metrics are aggregated, typically summed, across grouped rows.
Use Get dataset config to discover which columns a given dataset exposes.
Subgroups
Each metric column can optionally be subgrouped by one or more label columns. Subgrouping pivots the metric, producing one output column per unique subgroup value.
For example, subgrouping sold_product_revenue_net by location produces a separate revenue column for each location in the result. Subgroups only apply to metric columns and must reference valid label column names.
Cell types
Every cell in the response carries a type string:
| Type | Description |
|---|---|
string |
Text value (names, identifiers, dates) |
numeric |
Integer, decimal, or percentage |
dollars |
Monetary value in dollars |
timestamp |
RFC 3339 timestamp |
undefined |
Type could not be determined |
Discovery workflow
The API is built around three steps:
1. List datasets → get available dataset names
2. Get dataset config → get available columns for a dataset
3. Get dataset → query data with selected columns and date range
A typical integration calls List datasets, picks a dataset, calls Get dataset config to enumerate columns, then constructs a Get dataset request with the columns it wants, optional subgroups, and a date range.
Date ranges and record updates
Every Get dataset call takes an optional start/end date range and returns a fresh snapshot for that window. Both are optional, so leaving them out returns the dataset's full history, which you then walk with pagination. There is no "updated since last export" or changed-records feed, and no separate stream of corrections to older records, the same as the Report Builder.
So if a record from a past period is edited or corrected after you've already pulled it, the way to pick that up is to re-pull the window it falls in. The common pattern is a trailing re-pull: on each run, re-request a rolling window (say, the last 90 days) rather than only the dates since your last export, so late edits and corrections land in the next pull.
The request body has no row filter, so you narrow results the same way you do in the Report Builder: include the dimension columns you care about (and a date range), then group or filter the returned rows in your own code. To report on open purchase orders, for example, pull order_products with the columns you need and scope to open orders on your side.
Endpoints
List datasets
Returns every available dataset name as an array of strings.
Request
GET /v1/{subdomain}/datasets
| Path parameter | Type | Description |
|---|---|---|
subdomain |
string | Your Rundoo subdomain |
Response
{
"datasets": [
"aging_by_transaction",
"customers",
"gift_card_balances",
"interactions",
"inventory",
"inventory_counts",
"jobs",
"order_products",
"price_rules",
"product_costs",
"products",
"sales",
"shipment_products",
"sold_product_pricing_tiers",
"sold_products",
"spiffs",
"staff",
"tags",
"tax_rate",
"taxable_transactions",
"transactions",
"transfer_products",
"vendors"
]
}
Get dataset config
Returns the list of column names available for a specific dataset. Use these names verbatim in Get dataset request bodies.
Request
GET /v1/{subdomain}/datasets/{dataset_name}/config
| Path parameter | Type | Description |
|---|---|---|
subdomain |
string | Your Rundoo subdomain |
dataset_name |
string | A dataset name from List datasets |
Response
{
"columns": [
{ "name": "product" },
{ "name": "product_name" },
{ "name": "product_department" },
{ "name": "location" },
{ "name": "sold_product_revenue_net" },
{ "name": "sold_product_quantity_sold" },
{ "name": "sold_product_gross_profit" },
{ "name": "sold_product_gross_margin" }
]
}
The strings returned here are the exact values to use in the column and subgroups fields of the Get dataset body.
Get dataset
Queries a dataset with the specified columns, optional subgroups, and date range. Returns tabular data as columns and rows.
Request
POST /v1/{subdomain}/datasets/{dataset_name}
Content-Type: application/json
| Path parameter | Type | Description |
|---|---|---|
subdomain |
string | Your Rundoo subdomain |
dataset_name |
string | A dataset name from List datasets |
Request body
{
"start": "2026-01-01T00:00:00Z",
"end": "2026-12-31T23:59:59Z",
"page": 0,
"column_configs": [
{ "column": "product_name" },
{ "column": "location" },
{
"column": "sold_product_revenue_net",
"subgroups": ["location"]
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
start |
string (RFC 3339) | No | Start of the date-range filter, inclusive |
end |
string (RFC 3339) | No | End of the date-range filter, inclusive |
page |
integer | No | Which page of results to return, 0-indexed. A top-level field alongside start/end, not inside column_configs. Valid values are 0 to page_count - 1 (see the response). Omit it to get the first page. See Pagination. |
column_configs |
array | Yes | Column configurations to include in the output |
column_configs[].column |
string | Yes | A column name from Get dataset config |
column_configs[].subgroups |
array of strings | No | Label columns to subgroup this metric by (metric columns only) |
Response
{
"columns": [
{ "name": "Product Name" },
{ "name": "Location" },
{
"name": "Sold Product Revenue Net",
"subgroup_values": ["Main Store", "Warehouse"]
}
],
"rows": [
{
"cells": [
{ "type": "string", "value": "Premium Paint 1gal" },
{ "type": "string", "value": "Main Store" },
{ "type": "dollars", "value": "1250.00" },
{ "type": "dollars", "value": "430.50" }
]
},
{
"cells": [
{ "type": "string", "value": "Brush Set Pro" },
{ "type": "string", "value": "Warehouse" },
{ "type": "dollars", "value": "0.00" },
{ "type": "dollars", "value": "89.99" }
]
}
],
"page_count": 1
}
| Response field | Type | Description |
|---|---|---|
columns |
array | Ordered output column headers (with subgroup_values when a column is subgrouped) |
rows |
array | The data rows, each a cells array matching the columns |
page_count |
integer | Total number of pages available for this query. Valid page values are 0 to page_count - 1. |
When a column is subgrouped, it expands into multiple output cells per row, one for each subgroup value. The total cell count per row equals 1 for each non-subgrouped column plus the count of subgroup_values for each subgrouped column.
Pagination
Large results come back in pages of up to 25,000 rows each. Every Get dataset response includes a top-level page_count telling you how many pages the query produced.
To walk through them, pass a top-level page field in the request body (a sibling of start/end, not inside column_configs). It's 0-indexed, so valid values run from 0 to page_count - 1. Omit it and you get page 0.
{
"page": 0,
"column_configs": [ { "column": "customer_name" } ]
}
A typical loop: request page: 0, read page_count from the response, then request page: 1, page: 2, … up to page_count - 1.
Pulling a full history. Because start and end are optional, you can leave them both out to query the entire history of a dataset (for example, every customer), then page through from 0 to page_count - 1. There is no longer a ~1,000-record cap on results.
Error handling
All errors are JSON with an error field and an HTTP status code.
{
"error": "description of the error"
}
| Status | Meaning | Common cause |
|---|---|---|
400 |
Bad Request | Unknown dataset, unknown column, invalid timestamp, malformed JSON, or a page beyond the available range |
401 |
Unauthorized | Missing or invalid bearer token |
403 |
Forbidden | Token lacks permission for this resource |
404 |
Not Found | Subdomain doesn't exist |
500 |
Internal Server Error | Unexpected error. Retry, then contact Support if it persists |
Example error bodies:
{ "error": "unknown dataset \"nonexistent\"" }
{ "error": "unknown column \"not_a_real_column\"" }
{ "error": "invalid start timestamp: parsing time \"not-a-date\" ..." }
{ "error": "requested page exceeds page count" }
{ "error": "company 'badsubdomain' not found" }
Rate limits
There's no published rate limit today. If you hit one, the response will surface it, and it's worth a note to support@rundoo.ai so we can right-size the cap for your workload.
Common patterns
curl
# List every dataset for the acme subdomain
curl -H "Authorization: Bearer $RUNDOO_API_TOKEN" \
https://eapi.rundoo.app/v1/acme/datasets
# Pull revenue by location for 2026 YTD
curl -X POST \
-H "Authorization: Bearer $RUNDOO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"start": "2026-01-01T00:00:00Z",
"end": "2026-12-31T23:59:59Z",
"column_configs": [
{ "column": "location" },
{ "column": "sold_product_revenue_net" }
]
}' \
https://eapi.rundoo.app/v1/acme/datasets/sold_products
Python
import os
import requests
TOKEN = os.environ["RUNDOO_API_TOKEN"]
BASE = "https://eapi.rundoo.app/v1/acme"
HEAD = {"Authorization": f"Bearer {TOKEN}"}
# Discover columns for sold_products
config = requests.get(f"{BASE}/datasets/sold_products/config", headers=HEAD).json()
print([c["name"] for c in config["columns"]])
# Pull revenue and gross margin by product, for Q1
body = {
"start": "2026-01-01T00:00:00Z",
"end": "2026-03-31T23:59:59Z",
"column_configs": [
{"column": "product_name"},
{"column": "sold_product_revenue_net"},
{"column": "sold_product_gross_margin"},
],
}
resp = requests.post(f"{BASE}/datasets/sold_products", headers=HEAD, json=body).json()
for row in resp["rows"]:
print([cell["value"] for cell in row["cells"]])
Google Sheets / Excel
Both Sheets (IMPORTDATA, Apps Script UrlFetchApp) and Excel (Power Query → From Web with custom headers) can pull JSON from eapi.rundoo.app directly. Drive the bearer token from a named secret in Apps Script or Power Query rather than pasting it into a cell.
