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.