xPlantAPI

JavaScript SDK

@shmaplex/xplant-sdk: a typed server-side client for Node.js 18+ and other fetch runtimes, with retries, timeouts, idempotency and paging built in.

npm install @shmaplex/xplant-sdk

The source, issues and full reference live at github.com/shmaplex/xplant_sdk. Every endpoint page in the API reference shows the SDK call next to curl and Python.

These docs match @shmaplex/xplant-sdk 0.6.0. The SDK runs on Node.js 18+ and any runtime with standard fetch: Deno, Bun, edge and serverless functions. Call the API from your server, not a web page. The API doesn't send CORS headers, and keys don't belong in front-end code.

Create a client

import { XPlantClient } from "@shmaplex/xplant-sdk";

// On a server or in a script: a workspace key.
const client = new XPlantClient({ apiKey: process.env.XPLANT_API_KEY });

// On a device: a device token.
const device = new XPlantClient({ deviceToken: process.env.XPLANT_DEVICE_TOKEN });
Option
apiKeyA workspace key (xpk_…).
deviceTokenA device token (xpd_…). Give exactly one of apiKey or deviceToken. The client refuses a deviceToken that doesn't start with xpd_, so a workspace key can't end up on a device by mistake.
baseUrlDefaults to https://app.xplantpro.com. You shouldn't need to change it.
retrytrue, or { maxRetries, baseDelayMs, maxDelayMs }. Off unless set.
timeoutMilliseconds per attempt. Defaults to 60 seconds; 0 means no timeout. Can also be set per call.
fetchYour own fetch, for testing or unusual runtimes.

Lists and paging

Awaiting a list method gives you one page, as an array. Iterating it gives you every row, fetched a page at a time as you go:

// One page.
const plants = await client.plants.list({ limit: 200 });

// Every row.
for await (const plant of client.plants.list()) {
  console.log(plant.id);
}

// Page by page, with a cursor you can save and resume from.
for await (const { data, nextCursor, hasMore } of client.explants.list().pages()) {
  await save(data, nextCursor);
}
const next = await client.explants.list({ cursor: savedCursor });

Every list pages by cursor, including devices.list(), devices.listTokens(deviceId) and sensorReadings.list(). Pass request options such as { signal } after the list parameters: devices.list(params, options), devices.listTokens(deviceId, params, options). devices.get(id) searches every page. See Pagination.

Errors

import { XPlantError, XPlantConnectionError, XPlantTimeoutError } from "@shmaplex/xplant-sdk";

try {
  await client.tasks.create({ title: "Check jar 47" });
} catch (err) {
  if (err instanceof XPlantError) {
    // The API answered with an error.
    console.log(err.status, err.code, err.message, err.retryAfter, err.requestId);
  } else if (err instanceof XPlantTimeoutError) {
    // No answer within the timeout.
    console.log(`Timed out after ${err.timeout} ms`);
  } else if (err instanceof XPlantConnectionError) {
    // Never got an answer; the underlying fetch error is on err.cause.
  }
}

Branch on err.code. err.message includes the API's error text, which may be reworded. XPlantTimeoutError is a kind of XPlantConnectionError, so one check covers both.

Request IDs

Every response carries an X-Request-Id. The SDK exposes it as requestId: on errors (err.requestId), and on each page from .pages() (page.requestId). Log it, and quote it when you contact support.

try {
  await client.tasks.get(taskId);
} catch (err) {
  if (err instanceof XPlantError) console.error(`${err.code} (request ${err.requestId})`);
}

Retries

With retry on, the client:

  • waits out Retry-After on 429 RATE_LIMIT_EXCEEDED and 409 IDEMPOTENCY_IN_FLIGHT, up to maxDelayMs;
  • backs off exponentially on 502, 503, 504 and connection errors, for reads and for writes that are safe to replay;
  • on endpoints that honour idempotency, generates an Idempotency-Key if you didn't pass one, and reuses it across its own attempts.

Idempotency

Pass { idempotencyKey } as the last argument to any write:

await client.sopRuns.start({ sop_id: sopId }, { idempotencyKey: "station-3-wk38-start" });

Buffering sensor readings on a device

For a device posting readings continuously, the buffer batches, retries and de-duplicates for you:

const device = new XPlantClient({ deviceToken: process.env.XPLANT_DEVICE_TOKEN });

const buffer = device.sensorReadings.buffer({
  flushIntervalMs: 30_000,
  maxBatch: 500,
  onError: (err) => console.error(err),
});

buffer.add({ device_id: process.env.XPLANT_DEVICE_ID, type: "temperature", value: 23.4, unit: "C" });

// On shutdown, send what's left:
await buffer.close();

sensorReadings.create() resolves to the stored reading, or to null when the reading duplicated one already stored (same device, external_id and recorded_at).

The buffer stamps recorded_at and external_id on each reading, keeps readings through outages (up to 10,000), and drops and reports readings that are invalid. The queue is held in memory only, so readings still queued when the process dies are lost. See Sensors and devices.

Resources

ResourceMethods
client.meget
client.workspaceslist
client.plantslist, get, findByExternalId, create, update
client.explantslist, get, findByExternalId, create, update
client.transferslist, create
client.stageslist, advance
client.eventslist
client.taskslist, get, create, update
client.taskDemandlist, record
client.sopslist, get
client.sopRunsstart, get, recordStepEvent, recordMeasurement
client.labelsresolve, recordScan
client.deviceslist, get, register, heartbeat, listTokens, createToken, revokeToken, recordEvent
client.sensorReadingslist, create, createBatch, buffer
client.equipmentlist, get, listEvents, recordEvent
client.contaminationslist, get, create
client.commentslist, create
client.assetslist, get, create
client.mediaRecipeslist, get, create, update
client.pricinglistCultureLines, listEvents
client.commercelistOrderLines, getSellThrough

A device client can only call sensorReadings.create, sensorReadings.createBatch, sensorReadings.buffer, devices.heartbeat and devices.recordEvent, and only for its own device.

Other languages

There's no official SDK for other languages yet. The API is plain HTTPS and JSON, and every reference page has a Python example using requests. For a generated client, use the OpenAPI spec.

Edit on GitHub

On this page