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-sdkThe 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 | |
|---|---|
apiKey | A workspace key (xpk_…). |
deviceToken | A 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. |
baseUrl | Defaults to https://app.xplantpro.com. You shouldn't need to change it. |
retry | true, or { maxRetries, baseDelayMs, maxDelayMs }. Off unless set. |
timeout | Milliseconds per attempt. Defaults to 60 seconds; 0 means no timeout. Can also be set per call. |
fetch | Your 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-Afteron429 RATE_LIMIT_EXCEEDEDand409 IDEMPOTENCY_IN_FLIGHT, up tomaxDelayMs; - backs off exponentially on
502,503,504and connection errors, for reads and for writes that are safe to replay; - on endpoints that honour idempotency, generates an
Idempotency-Keyif 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
| Resource | Methods |
|---|---|
client.me | get |
client.workspaces | list |
client.plants | list, get, findByExternalId, create, update |
client.explants | list, get, findByExternalId, create, update |
client.transfers | list, create |
client.stages | list, advance |
client.events | list |
client.tasks | list, get, create, update |
client.taskDemand | list, record |
client.sops | list, get |
client.sopRuns | start, get, recordStepEvent, recordMeasurement |
client.labels | resolve, recordScan |
client.devices | list, get, register, heartbeat, listTokens, createToken, revokeToken, recordEvent |
client.sensorReadings | list, create, createBatch, buffer |
client.equipment | list, get, listEvents, recordEvent |
client.contaminations | list, get, create |
client.comments | list, create |
client.assets | list, get, create |
client.mediaRecipes | list, get, create, update |
client.pricing | listCultureLines, listEvents |
client.commerce | listOrderLines, 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.
List price changes
Every change to a culture line's price, newest first: what it moved from, what it moved to, when, who or what moved it. Filter with plant_id for one culture line and from and to for a period.
Hardware and examples
Open-source firmware, gateways and example scripts for connecting lab devices to xPlant.