<!-- https://leantools.ai/doc/data · HTML twin of this page; the whole documentation: https://leantools.ai/llms-full.txt -->
# LeanData

Agents

LeanData serves queryable datasets published by data providers: tables (filter, order, page) and document collections (list, fetch). A dataset is addressed by `{provider}.{dataset}` and priced per dataset: one `call` plus the rows returned, and for subscription datasets a monthly charge.

## Endpoints

 (GET)/api/v1/data?domain=&country=&kind=table|document&provider=catalogue with prices and access state

 (GET)/api/v1/data/subscriptionsyour dataset subscriptions

 (GET)/api/v1/data/{dataset}columns, document types, examples, operators, capabilities

 (POST)/api/v1/data/{dataset}/queryJSON page (default), NDJSON (`?format=ndjson`) or CSV (`?format=csv`)

 (GET)/api/v1/data/{dataset}/documents?type=&search=&take=&pageToken=document summaries

 (GET)/api/v1/data/{dataset}/documents/{id}?redirect=a document, inline or with a `downloadUrl`

 (GET)/api/v1/data/downloads/{ticket}the bytes behind a `downloadUrl` (no bearer)

 (POST)/api/v1/data/{dataset}/subscribe201 subscription (first month charged now)

 (DELETE)/api/v1/data/{dataset}/subscribecancel at the end of the period

The key must be scoped to the `Data` service; catalogue, describe and subscription calls are free. Over MCP the same operations are `data.list`, `data.describe`, `data.query`, `data.documents.list`, `data.documents.get`, `data.subscribe` and `data.unsubscribe`, with the dataset as an argument.

## Catalogue and describe

`GET /api/v1/data` lists the enabled datasets your workspace may use (a dataset licensed per country is hidden when your workspace's country is not allowed). Each item carries `pricing` at your rates and `access`, which says whether a query would be admitted right now:

```
{ "items": [ {
    "key": "leantools-usage.service-usage", "provider": "leantools-usage", "name": "Service usage", "kind": "table",
    "domains": ["Usage"], "coverage": ["GLOBAL"], "columns": ["day", "serviceKey", "meterKey", "requests", "credits"],
    "pricing": { "callCredits": 0, "rowCreditsPer1000": 0, "monthlyCredits": null, "subscriptionRequired": false, "subscribed": false, "summary": "0 credits/call, 0 credits/1,000 rows" },
    "access": { "allowed": true, "reason": null, "requiresSubscription": false, "subscribed": false, "subscriptionStatus": null, "currentPeriodEndUtc": null }
} ], "count": 1 }
```

`GET /api/v1/data/{dataset}` adds `columnDefinitions[{ name, type, description, isKey, isFilterable, isSortable, isNullable }]`, `documentTypes`, `examples` (ready-to-send query bodies), `operators` and `capabilities` (`supportsFilters`, `supportsOrderBy`, `supportsFieldSelection`, `supportsNativeQuery`, `supportsTotalCount`, `maxPageSize`, `defaultPageSize`).

## Query (JSON page)

```
curl -s https://YOUR-HOST/api/v1/data/leantools-usage.service-usage/query \
  -H "Authorization: Bearer lt-..." -H "Content-Type: application/json" \
  -d '{ "filters": [ { "field": "serviceKey", "op": "eq", "value": "Search" } ], "fields": ["day", "requests", "credits"],
        "orderBy": "day", "descending": true, "take": 100, "includeTotalCount": true }'
```

200

```
{
  "dataset": "leantools-usage.service-usage",
  "columns": [ { "name": "day", "type": "DateTime" }, { "name": "requests", "type": "Integer" }, { "name": "credits", "type": "Decimal" } ],
  "rows": [ [ "2026-09-27T00:00:00+00:00", 41, 4.1 ], [ "2026-09-26T00:00:00+00:00", 12, 1.2 ] ],
  "nextPageToken": "MjAw", "totalCount": 233, "truncated": false,
  "usage": { "rows": 100, "credits": 0.11, "balance": 1874.25 }
}
```

- `filters[].op` is one of `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in` (array value), `contains`, `startsWith`, `between` (two-element array), `isNull` (no value); filters are combined with AND and only `isFilterable` columns are accepted (`400 invalid_filter`).
- `take` is clamped to the dataset's `maxPageSize` and the host's `LeanTools.Data.MaxRowsPerCall`; pass `nextPageToken` back as `pageToken` to continue. An empty body returns the first page at the default page size. `nativeQuery` is accepted only where `capabilities.supportsNativeQuery` is true.
- The body is limited to `LeanTools.Data.MaxQueryBodyBytes` (1 MiB); a page over `LeanTools.Data.MaxResponseBytes` (64 MiB) is refused with `413 response_too_large` (stream it instead).
- `usage.balance` is the balance after this call; `usage.credits` = the call price plus `rows / 1000 × row price`.

## Streaming (NDJSON) and CSV

Ask for `?format=ndjson` (or `?stream=true`, or `Accept: application/x-ndjson`) to receive rows as they are produced, one JSON document per line:

```
curl -N https://YOUR-HOST/api/v1/data/leantools-usage.service-usage/query?format=ndjson \
  -H "Authorization: Bearer lt-..." -H "Content-Type: application/json" -d '{ "take": 5000 }'
```

```
{"dataset":"leantools-usage.service-usage","columns":[{"name":"day","type":"DateTime"},{"name":"requests","type":"Integer"}]}
{"day":"2026-09-27T00:00:00+00:00","requests":41}
{"day":"2026-09-26T00:00:00+00:00","requests":12}
{"nextPageToken":null,"totalCount":null,"truncated":false,"usage":{"rows":2,"credits":0.1002,"balance":1874.15}}
```

A header line with the columns, one object per row keyed by column name, then a trailer with the page token and the usage block. If the provider fails after the header the stream ends with `{"error":{"code":"provider_error","message":"…","status":502},"usage":{…}}` and the rows already delivered are charged; if you disconnect mid-stream, the rows already sent are charged as well. `?format=csv` answers `text/csv` with a header row (RFC 4180 quoting; cells starting with `=`, `+`, `-`, `@` are prefixed with `'`); the usage block travels in the `X-LeanTools-Usage` HTTP trailer.

## Documents

`GET /api/v1/data/{dataset}/documents` lists a document dataset's items (`{ items[{ id, type, title, contentType, size, modifiedTime, metadata }], nextPageToken, totalCount, usage }`; billed one `call` plus one `row` per summary). `GET …/documents/{id}` returns one document (one `call` plus one `row`):

```
{ "dataset": "leantools-usage.monthly-statement", "id": "2026-08", "type": "statement", "title": "August 2026", "contentType": "text/markdown", "size": 2140,
  "content": "# Statement for August 2026\n...", "downloadUrl": null, "expiresUtc": null, "usage": { "rows": 1, "credits": 0, "balance": 1874.15 } }
```

Textual documents up to `LeanTools.Data.InlineDocumentBytes` (256 KiB) come back inline. Larger or binary documents carry `content: null` and a signed `downloadUrl` valid for `LeanTools.Data.DownloadTtlSeconds` (no bearer needed, range requests honoured); `?redirect=true` answers `302`. A tampered or expired link is `403 invalid_ticket`; the download itself is not billed.

## Subscriptions

A dataset with `access.requiresSubscription: true` refuses queries with `402 subscription_required` until the workspace subscribes, from the console (Tools & Connectors → Datasets) or the API:

```
curl -s -X POST https://YOUR-HOST/api/v1/data/market.prices/subscribe -H "Authorization: Bearer lt-..."
```

201

```
{ "subscription": { "datasetKey": "market.prices", "status": "active", "monthlyCredits": 5, "currentPeriodEndUtc": "2026-10-27T09:12:00+00:00" },
  "created": true, "notRequired": false, "usage": { "credits": 5, "balance": 1869.15 } }
```

The first month is charged immediately (`402 insufficient_credits` when the balance does not cover it) and access opens at once. Repeating the call on an active subscription answers `200` with `created: false`. The month renews automatically; a paid renewal that finds no credits suspends the subscription (`402 subscription_suspended`) until the workspace is topped up. `DELETE …/subscribe` answers `{ datasetKey, status: "cancelled", accessUntilUtc }`: access continues until the end of the paid period.

## Billing notes

Every query, listing or fetch writes one usage row under the `call` meter with the dataset key as qualifier and the rows delivered as `outputUnits`. A call refused before its first row (bad filter, no credits, no subscription, provider down) costs nothing; rows already delivered are always charged. A dataset whose `call` and `row` rates are both 0 (your own usage data, for instance) can be queried on an empty balance.

## Status codes

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request`, `invalid_query`, `invalid_filter`, `invalid_sort`, `invalid_fields`, `invalid_page_token`, `native_query_not_supported`, `documents_not_supported`, `not_subscribable` | Malformed body or format, a column, operator, page token or capability the dataset does not have |
| 401 | `authentication_error` | Missing or unknown bearer |
| 402 | `insufficient_credits`, `subscription_required`, `subscription_suspended` | No credits for a priced dataset; no (or a suspended) subscription |
| 403 | `service_not_allowed`, `dataset_disabled`, `dataset_unavailable`, `country_blocked`, `country_not_allowed`, `country_required`, `invalid_ticket` | Key scope, a dataset switched off, geography, a forged or expired download link |
| 404 | `dataset_not_found`, `document_not_found`, `not_subscribed` |  |
| 413 | `payload_too_large`, `response_too_large` | The query body, or a page / document over the host limit |
| 502 / 503 / 504 | `provider_error`, `provider_unavailable`, `provider_timeout` | The data provider failed, is down, or exceeded `LeanTools.Data.QueryTimeoutSeconds` |
