How to videos

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