# Record transfers and stages

> Log subcultures to fresh media and move plants and explants through tissue-culture stages from a script or a scanner.

Source: https://docs.xplantpro.com/docs/guides/transfers-and-stages

**Scopes:** `read:transfers`, `write:transfers` (and `read:plants` / `read:explants` to look records up). **Endpoints:** [Record a transfer](https://docs.xplantpro.com/docs/api/transfers-and-stages/create-transfer.md), [List transfers](https://docs.xplantpro.com/docs/api/transfers-and-stages/list-transfers.md), [Advance a stage](https://docs.xplantpro.com/docs/api/transfers-and-stages/create-stage.md), [List stages](https://docs.xplantpro.com/docs/api/transfers-and-stages/list-stages.md).

## 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:

**JavaScript**

```js
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 null
```

**curl**

```bash
curl "https://app.xplantpro.com/api/v1/explants?externalId=LINE-0412" \
  -H "Authorization: Bearer $XPLANT_API_KEY"
```

**Python**

```python
import os
import requests

BASE = "https://app.xplantpro.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['XPLANT_API_KEY']}"}

matches = requests.get(f"{BASE}/explants", headers=HEADERS, params={"externalId": "LINE-0412"}, timeout=10).json()["data"]
explant = matches[0] if matches else None
```

A scanner can also [resolve a label](https://docs.xplantpro.com/docs/guides/label-scanning.md) 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**

```bash
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"
  }'
```

**JavaScript**

```js
const transfer = await client.transfers.create({
  explant_id: explant.id,
  from_location: "Shelf A2",
  to_location: "Shelf B1",
  notes: "Routine subculture",
});
```

**Python**

```python
transfer = requests.post(
    f"{BASE}/transfers",
    headers=HEADERS,
    json={"explant_id": explant["id"], "from_location": "Shelf A2", "to_location": "Shelf B1"},
    timeout=10,
).json()["data"]
```

Optional fields: `transfer_date`, `from_location`, `to_location`, `transfer_cycle`, `notes`, 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.

## 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.

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**

```bash
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"}'
```

**JavaScript**

```js
await client.stages.advance({ explant_id: explant.id, stage: "multiplication" });
```

**Python**

```python
requests.post(
    f"{BASE}/stages",
    headers=HEADERS,
    json={"explant_id": explant["id"], "stage": "multiplication"},
    timeout=10,
).raise_for_status()
```

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](https://docs.xplantpro.com/docs/guides/change-history.md) as `transfer` and `stage_change` events.

```bash
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](https://docs.xplantpro.com/docs/guides/label-scanning.md) 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](https://docs.xplantpro.com/docs/guides/sop-runs.md#bench-stations-and-keys).
