Record transfers and stages
Log subcultures to fresh media and move plants and explants through tissue-culture stages from a script or a scanner.
Scopes: read:transfers, write:transfers (and read:plants / read:explants to look records up). Endpoints: Record a transfer, List transfers, Advance a stage, List stages.
Find the record first
Transfers and stages belong to a plant or an explant. If your own system knows a batch by its own code, look it up with externalId rather than keeping a separate id mapping:
import { XPlantClient } from "@shmaplex/xplant-sdk";
const client = new XPlantClient({ apiKey: process.env.XPLANT_API_KEY });
const explant = await client.explants.findByExternalId("LINE-0412"); // record or nullA scanner can also resolve a label straight to the record.
Record a transfer
A transfer is a subculture onto fresh media. Give exactly one of plant_id or explant_id. The transfer cycle counts up from the record's history unless you set transfer_cycle yourself.
curl -X POST https://app.xplantpro.com/api/v1/transfers \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"explant_id": "7c1d9e2a-3b4f-4a5c-8d6e-1f2a3b4c5d6e",
"from_location": "Shelf A2",
"to_location": "Shelf B1",
"notes": "Routine subculture"
}'Optional fields: transfer_date, from_location, to_location, transfer_cycle, notes, custom_fields (your lab's own fields, as on plants and explants), and status: completed (the default) for a transfer that's done, or pending for one planned for transfer_date, which counts as done only once someone marks it completed in xPlant.
transfer_date is a calendar day, so send the day itself, such as 2026-09-24. A full timestamp is accepted too, but its day is read in UTC, so 2026-09-26T08:00:00+09:00 lands on 25 September. Transfers come back with transfer_date as a timestamp; take its first ten characters as the day rather than converting it to your time zone, which can shift it by a day.
Advance a stage
One call completes the current stage and makes the new one current. stage is required; give exactly one of plant_id or explant_id.
Send stage as a key from your lab's stage list. Plants and explants have separate lists: an explant might move to multiplication or root_induction, a plant to production. A stage your lab doesn't use for that kind of record is refused with 422 VALIDATION_ERROR naming it, and nothing changes; an older display name is accepted and the response gives you the key to send next time.
A move happens completely or not at all: if it fails you get 500 STAGE_ADVANCE_FAILED, nothing has moved, and retrying with the same Idempotency-Key is safe.
Only the person who created the plant or explant, or a manager, can move its stage. Otherwise the answer is 403 STAGE_WRITE_FORBIDDEN; remember that a key acts with its owner's role.
curl -X POST https://app.xplantpro.com/api/v1/stages \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"explant_id": "7c1d9e2a-3b4f-4a5c-8d6e-1f2a3b4c5d6e", "stage": "multiplication"}'Optional fields: entered_on, room_id, notes.
Read the history
Both lists take one of plant_id or explant_id and return the most recent first. Transfers and stage moves also appear in change history as transfer and stage_change events.
curl "https://app.xplantpro.com/api/v1/transfers?explant_id=$EXPLANT_ID" -H "Authorization: Bearer $XPLANT_API_KEY"
curl "https://app.xplantpro.com/api/v1/stages?plant_id=$PLANT_ID" -H "Authorization: Bearer $XPLANT_API_KEY"A transfer counter at the bench
A common build is a scanner plus a button: scan the vessel, press "transferred". The scanner resolves the label to a record, and the button posts a transfer for it.
Device tokens can't record transfers, so a station like this needs a workspace key. Give the station its own key holding only read:labels, write:label_scans and write:transfers, so that a lost station exposes as little as possible and can be revoked on its own. See Bench stations and keys.