Skip to content

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.

Get a test key

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.

POST /v1/datasets
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"
  }'
201 Created
{
  "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.

POST /v1/datasets/{id}/versions
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
      }
    ]
  }'
201 Created
{
  "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.

POST /v1/datasets/{id}/metrics
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" }'
200 OK
{
  "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.

  • periods is the axis. Every row's values array is dense and aligned to it, index for index — no sparse maps, no missing keys to defend against.
  • rowId is stable across releases. Match on it, never on displayName, which is human-facing text we may improve.
  • formatType tells you how to render a raw number. Nothing on the wire is pre-formatted and no locale is baked in.
  • null means 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