Skip to content

Datasets, versions, facts

Three objects hold the whole model. Understanding what a customer-month fact is explains most of the API's behaviour at once — what a query costs, why versions are immutable, and why some metrics cannot be cohorted.

A dataset is one company

A dataset is the tenant boundary: one company whose metrics are being computed. If you are a platform serving many customers, you create one dataset per customer and address each by externalRef — your own identifier, which means you never have to store or reconcile ours.

Datasets are first-class and isolated from each other, not rows in a shared table separated by a flag. A query is always scoped to exactly one dataset, and the credential that made the request determines which datasets exist at all as far as that request is concerned.

A dataset declares its input model once, at creation, and keeps it. Everything downstream — what is computable, what is cohortable, what a segment can be sliced by — follows from that choice.

Versions are immutable

A dataset holds a series of versions. Each is a complete statement of the data at a point in time; one of them is active. You do not update rows, you post a new version.

A version's status is one of four. building is what a large upload returns while the engine works — the previously active version keeps answering queries throughout. active is the one being served. invalid is one that failed validation and never became active, and superseded is one a later version replaced. The last three are terminal, so a polling loop has somewhere to stop.

A dataset also carries modepersistent, or ephemeral with an expiresAt. That is about how long the dataset lives, and is unrelated to livemode, which is the test-versus-live boolean.

Immutability is not a purity argument here, it is what makes three other things work: a board pack can be recomputed from exactly the rows that produced it, an upload either lands whole or changes nothing, and caching needs no invalidation because every cache key contains the version id and a new upload rotates all of them at once.

The customer-month fact

Underneath both models, the engine works on one atom: the customer-month fact — what one customer was worth in one month. Contracts expand into customer-months. Transactions aggregate into them. By the time any metric is computed, both look identical.

expansion
Contract C-1041
  customer      Northwind Logistics
  revenueModel  RECURRING
  signedDate    2026-01-14
  startDate     2026-02-01
  endDate       2027-01-31
  term          12
  tcv           84000

expands to 12 customer-month facts

  2026-02  Northwind Logistics  7000
  2026-03  Northwind Logistics  7000
  ...
  2027-01  Northwind Logistics  7000

Four things decide how much of that expansion happens, and they do not fail alike. A tcv of zero or a missing signedDate removes the contract from everything. A missing term, or a one-time revenueModel, keeps bookings and revenue while zeroing every recurring measure — which is the harder failure to notice. Sending data has the breakdown.

The consequence worth internalising: cost is bounded by customers times months, never by the number of rows you send. Fifty thousand transactions from two thousand customers over three years is seventy-two thousand customer-months, and the fifty thousand never reach the metric math. Uploads are capped at 50,000 rows and 10MB inline — 20MB by workbook — so at real volumes you send periodically rather than all at once. This is why a marketplace with an enormous event stream and a B2B company with four hundred contracts are the same kind of problem to us.

A one-year contract on a monthly axis is twelve facts. The 380-contract sample dataset behind the figures on these pages is 4,234 facts. That number, not your row count, is what a query is priced against.

Two axes of capability

"706 metric definitions" describes the engine. What your dataset can answer is narrower, and it is narrowed along two independent axes that are easy to conflate.

Computable

Depends on your input model. Contracts unlock everything. Transactional data has no contract term, so bookings, contract statistics, CMRR and contracted users are not merely empty — they are undefined.

Cohortable

Depends on the metric itself. Exactly seven measures decompose per customer per month and can therefore be cohorted. The rest cannot, no matter how rich your data is. Cohorted retrieval is not yet exposed on the metrics endpoint — today this tells you which measures qualify, which is what you can act on.

A metric can be fully computable and still not be cohortable — bookings is the clean example. A booking posts entirely in the month it was signed, so every later cohort offset is zero by construction: the curve is a spike and then nothing. That is not missing work, it is what the quantity means.

The catalog returns both axes. Computability it reports from the grid your data actually produced; the cohortable list is currently the same fixed set for every dataset, so do not read it as a capability check on your input model.

Stateful and stateless operation

Everything above describes stateful operation — unrelated to the mode field on a dataset, which is about expiry, and unrelated again to test versus live. You send data once, we hold it, you ask questions against it. It is the default and the recommended mode, and it is the only one available today.

A stateless mode is planned, for callers whose binding constraint is retention rather than convenience — send the data with the request, get metrics back, keep nothing. The tradeoffs are published here rather than discovered later, because they are real and the two modes are not equals.

Stateful · available Stateless · planned
Data sent Once per version On every request
We retain your data Yes, until deleted or expired No
Multi-call workflows Natural Impractical — state is lost between calls
Request size 10MB inline, 20MB by workbook, 50,000 rows Bounded by payload limits
Cost profile Storage plus compute Compute plus repeated transfer
Caching Full, keyed by immutable version None — nothing to key on
Compliance posture DPA, retention policy, deletion API “We never keep your data”
Best for Platforms, recurring reporting, chat sessions One-shot valuations, privacy-constrained callers

The row that decides it for most integrations is the third one. Anything conversational or iterative — a dashboard, a chat session, a segment sweep you refine — makes many calls about the same data, and stateless loses everything between them.

Isolation

Tenant isolation is enforced twice, by two mechanisms that do not share a failure mode.

  • In the application. Every data access is scoped by the partner identity taken from the validated credential — never from a path parameter or a request body.
  • In the database. Row-level security policies key every table that holds customer data to a per-connection identity, so a query that somehow escaped the first layer still returns nothing.

Each layer is tested with the other disabled, so neither is load-bearing on its own. The same boundary separates test and live: a dataset created with a test key is not visible to a live key, and there is no promotion path between them.