From nothing to a computed metric
Three calls: create a dataset, put a version of your data in it, ask a question. A test key is enough for all three, and you can run them against your own hand-written row before you have anything real to send.
Get a test key
REST keys are issued by us against your account rather than minted from a form, so this step involves a message: ask for one from the address on your Plantactic account. If what you want is Claude talking to your data rather than your own client calling it, the connector needs no key at all — approving it provisions the access.
A key that starts with ptx_test_ works in an account that is never billed and
cannot reach live data. It starts empty, so the three calls below are what you do with it. Datasets and versions it returns carry
livemode: false — assert on that, not on mode, which is a different
field about whether a dataset expires.
A live key — ptx_live_ — is issued the same way, against the same account, and the
two prefixes are never interchangeable: neither is a dataset created under one. Read
Test mode for what that boundary buys you.
Every request below sends its key in an Authorization: Bearer header. Keys are
credentials for your whole organisation, not for one user — keep them server-side.
1 · Create a dataset
A dataset is one company whose metrics you want computed. If you are a platform computing metrics for many of your own customers, you create one dataset per customer and address each by your own identifier — we never need to learn your naming.
inputModel is the one field you cannot change later, because it decides which of the
metric definitions can be computed at all. contracts is the richest and unlocks
everything; revenue_events is the other one — see
Sending data for what each forecloses.
curl -X POST https://ptx-api.plantactic.com/v1/datasets \
-H "Authorization: Bearer ptx_test_4e91c8..." \
-H "Idempotency-Key: 7c1a-acme-create" \
-H "Content-Type: application/json" \
-d '{
"externalRef": "acme-corp",
"name": "Acme Corp",
"inputModel": "contracts"
}' {
"datasetId": "9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77",
"externalRef": "acme-corp",
"name": "Acme Corp",
"inputModel": "contracts",
"mode": "persistent",
"activeVersionId": null,
"expiresAt": null,
"livemode": false,
"createdAt": "2026-08-31T06:16:33.438658Z"
} externalRef is yours and must be unique within your account — send the same one
twice and you get a 409 rather than a second dataset, which is usually what you
want and occasionally a surprise.
Keep the datasetId. The next two calls put it in the path, and the
id below is the one this sample got — yours will differ. Paths take a bare UUID and 404 on
anything else, so substituting it is not optional.
Idempotency-Key is required on every mutating call. Use a value unique to the
operation — a UUID is fine — rather than the literal above. Replay the same key with the same
body and you get the original result back; replay it with a different body, which is
what happens when you fix a row and retry, and you get a 422 telling you to use a
new key. Replays are matched for 24 hours.
2 · Upload a version
Data goes in as an immutable version. You never patch rows; you send a new version and it becomes active. That is what lets you recompute last quarter's board pack from exactly the data that produced it.
curl -X POST \
https://ptx-api.plantactic.com/v1/datasets/9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77/versions \
-H "Authorization: Bearer ptx_test_4e91c8..." \
-H "Idempotency-Key: 7c1a-acme-v1" \
-H "Content-Type: application/json" \
-d '{
"contracts": [
{
"contractId": "C-1041",
"customer": "Northwind Logistics",
"product": "Platform",
"revenueModel": "RECURRING",
"signedDate": "2026-01-14",
"startDate": "2026-02-01",
"endDate": "2027-01-31",
"term": 12,
"licenses": 100,
"tcv": 84000
}
]
}' {
"versionId": "3b71a0c9-2e64-4b18-8f3a-51d7c9e02a6b",
"datasetId": "9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77",
"status": "active",
"rowCount": 1,
"createdAt": "2026-08-31T06:16:33.510507Z",
"activatedAt": "2026-08-31T06:16:33.522739Z",
"livemode": false
}
Past two thousand rows this comes back 202 with the version still
building, and a workbook upload always does. Poll the version —
GET /v1/datasets/{id}/versions/{versionId} — until
status is active or invalid. It is driven by size, not by
which key you hold, so a test key meets it too.
One contract is enough to get a response, though not an interesting one. What comes back is
rowCount — the rows accepted — and behind it the engine has expanded that one-year
contract into twelve customer-month facts. It is those facts, never your row count, that bound
what a query costs.
Send term. It is optional, a row without it is accepted without
complaint, and the result is worse than an obvious failure: the contract still lands in
bookings TCV and in recognized revenue, so those look right — while
every ARR, MRR, CARR, CMRR, logo and user figure it should have produced is zero.
A grid that is wrong in one corner gets rationalised; a grid that is empty gets investigated.
Three other fields gate the same way, each differently: a tcv of zero or a missing
signedDate removes the row from everything, and a
revenueModel containing ONE_TIME keeps bookings and revenue while
dropping the recurring measures. Check all four before you trust a grid.
licenses is a different matter: it drives user counts and the per-unit metrics, and
leaving it out costs you those rather than the revenue ones.
If any row fails validation, nothing is written and you get a 422 carrying
every problem in the payload, not the first one. Fix them in one pass and send
again.
3 · Ask for metrics
Now ask a question. The body takes one field — horizon, the month to compute as
of — and the axis is derived from it. Computation happens on demand: there is no precompute
step to wait for and no webhook to subscribe to.
Omit the body entirely and the horizon is the current month. What you get back is the whole grid — seventeen sections, every metric the dataset can compute — rather than a subset you asked for; narrowing a query to particular metric groups is not something this endpoint does yet.
curl -X POST \
https://ptx-api.plantactic.com/v1/datasets/9f2c4b1e-7a83-4d02-9c15-6b0e2a1d4f77/metrics \
-H "Authorization: Bearer ptx_test_4e91c8..." \
-H "Content-Type: application/json" \
-d '{ "horizon": "2026-06" }' {
"horizon": "2026-06",
"axisStart": "2026-01",
"axisEnd": "2027-01",
"months": 13,
"periods": ["2026-01", "2026-02", ..., "2027-01"],
"sections": [
{
"sectionId": "arr_waterfall",
"title": "Live ARR",
"rows": [
{
"rowId": "arr_waterfall.starting_live_arr",
"metric": "STARTING_LIVE_ARR",
"displayName": "Starting ARR",
"formatType": "ACCURATE_CURRENCY_VALUE",
"preset": null,
"values": [0.0, 0.0, 84000.0, 84000.0, ...],
"secondValues": null,
"formatType2": null
},
{
"rowId": "arr_waterfall.new_live_arr",
"metric": "NEW_LIVE_ARR",
"displayName": "New Live ARR",
"formatType": "ACCURATE_CURRENCY_VALUE",
"preset": "INDENT",
"values": [0.0, 84000.0, 0.0, 0.0, ...],
"secondValues": null,
"formatType2": null
}
]
}
]
} One row makes a thin grid. Upload a few dozen — invented is fine, and a test account is where to do it — before you judge whether the response shape suits your client.
Read the response
The response is deliberately boring, and every part of it is addressable by a stable key rather than by position in a list.
-
periodsis the axis. Every row'svaluesarray is dense and aligned to it, index for index — no sparse maps, no missing keys to defend against. -
rowIdis stable across releases. Match on it, never ondisplayName, which is human-facing text we may improve. -
formatTypetells you how to render a raw number. Nothing on the wire is pre-formatted and no locale is baked in. -
nullmeans the metric is not computable for that period. It never means zero. A burn multiple in a month where net new ARR was negative has no value, and saying so is more useful than printing a zero somebody puts in a board deck.
Querying metrics documents the full envelope, all four format types, and the row presets that carry structure — indentation, totals, and the rows that should read as a subtraction.
What changes for live
Almost nothing, which is the point. Swap ptx_test_ for ptx_live_ and
the same three calls run against your own data. The parts worth knowing before you do:
-
Live usage is billed on compute rather than request count. Every metrics response tells you
what it cost in
X-Plantactic-Compute-Units; nothing is refused for being expensive. - Test and live keys address separate datasets. A dataset created with a test key is not visible to a live key, and there is no promotion path between them. Create live datasets with a live key.
Where to go next
- Datasets, versions, facts Why the atomic unit is a customer-month, and what that means for cost and for what you can ask.
- Sending data The two input models, what each one forecloses, and the shape of a 422.
- Querying metrics The response envelope in full, plus the catalog and its two independent axes.
- API reference Every endpoint, parameter, and error, generated from the OpenAPI spec.