Rundocs › Integrations
External Data API
Last updated 2026-04-30
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.
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.
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",
"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 |
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" }
]
}
]
}
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.
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 |
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": "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.
