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.7.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.
Pacing a bulk job
client.rateLimit is the request budget the latest response reported, from its rate-limit headers: { limit, remaining, reset, observedAt }, or null.
for (const row of rows) {
await client.plants.create(row);
const budget = client.rateLimit;
if (budget && budget.remaining < 50) {
// `reset` is whole seconds from `observedAt` until the minute's budget refills
const waitMs = budget.reset * 1000 - (Date.now() - budget.observedAt);
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}limitandremainingare for whichever budget has fewer requests left: the key's (or device token's) 1,000, or the workspace's 3,000.- Every success and every
429updates it; other failures leave it as it was. - It's
nullbefore the first response, and after a success that carried no budget because the API couldn't read it. Treatnullas unknown, not unlimited. - The sensor readings budget isn't included.
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 with the stored reading, including when it duplicated one already stored (nothing new is written). devices.recordEvent() takes an optional external_id and behaves the same way. The meta.duplicate flag that marks a repeat is in the response envelope; call client.requestEnvelope() when you need meta.
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, listEvents, recordStepEvent, recordMeasurement, complete |
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.