List change history
What has happened to the workspace's plants or explants, oldest first: transfers, stage changes, contamination records, observations, printed labels.
https://app.xplantpro.com/ api/ v1/ eventsread:eventsUse it to keep your own system in step with xPlant without downloading everything again:
- The first time, call with
entity=plantand followmeta.next_cursoruntil it comes backnull. Do the same withentity=explant. - Save the newest
created_atyou received for each. - Next time, pass that value as
sinceand follow the cursor again from the first page.
Several events can share one created_at — a batch import records many at once. The cursor keeps them in a fixed order, so a walk neither skips nor repeats one; still walk a since window to its last page before you move since forward, and skip any id you already hold.
The history is written by work done in xPlant. Transfers and stage moves recorded through this API's own endpoints do not add events here; their responses already carry what was saved.
See also: Pull change history.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
entity | "plant" | "explant" | Yes | Which history to read. Required: plant and explant histories are separate, so sync each with its own calls. |
since | string | No | Only events recorded after this moment, as an ISO 8601 timestamp such as 2026-09-24T00:00:00Z. Pass the newest created_at you already hold. |
limit | integer | No | Page size. Values above 200 are capped at 200. Default 50. From 1 to 200. |
offset | integer | No | Number of records to skip. Prefer cursor where a list offers it: an offset shifts when records are added ahead of it. Default 0. At least 0. |
cursor | string | No | Continue from the previous page: pass its meta.next_cursor unchanged, with the same filters. Treat it as opaque. Not combinable with offset. Up to 2048 characters. |
Example
curl "https://app.xplantpro.com/api/v1/events?entity=plant&since=2026-09-01T00%3A00%3A00Z" \
-H "Authorization: Bearer $XPLANT_API_KEY"Response
200 with { "ok": true, "data": … }. data is an array. To get the next page, pass meta.next_cursor back as cursor; it is null on the last page. See Pagination.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | The event's id. Events are never edited, so an id you already hold is the same event. |
entity_type | "plant" | "explant" | Yes | Whether the event is about a plant or an explant. |
entity_id | string (uuid) | Yes | The plant's or explant's id. |
stage_id | string | null (uuid) | Yes | The stage the event was recorded against, when there was one. |
event_type | string | Yes | What happened — for example transfer, stage_change, contamination, observation or label_print. New kinds can appear; treat one you do not recognise as informational. |
event_time | string | Yes | When it happened in the lab, as an ISO 8601 timestamp. |
recorded_by | string | null (uuid) | Yes | The workspace member who recorded it, when known. |
payload | any | Yes | The event's details, as a JSON value whose fields depend on event_type — a transfer, for example, carries its cycle, locations and vessel counts. Can be null. |
created_at | string | Yes | When xPlant recorded the event. Pass the newest one you hold back as since. |
{
"ok": true,
"data": [
{
"id": "4b3a2918-0716-4f5e-8d4c-3b2a19087f6e",
"entity_type": "explant",
"entity_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"stage_id": "6e5d4c3b-2a19-4807-96a5-b4c3d2e1f0a9",
"event_type": "transfer",
"event_time": "2026-09-24T00:00:00+00:00",
"recorded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
"payload": {
"transfer_id": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
"transfer_cycle": 5,
"from_location": "Growth room 1, shelf B2",
"to_location": "Growth room 1, shelf C1",
"observed_vessel_count": 12,
"plantlet_count": 48
},
"created_at": "2026-09-24T09:12:45.118Z"
}
]
}The envelope can also carry meta:
| Field | Type | Required | Description |
|---|---|---|---|
next_cursor | string | null | Yes | Pass as cursor to fetch the next page. null means this is the last page. |
| Response header | Meaning |
|---|---|
X-Request-Id | Identifies this request. Include it when you contact support. |
Errors
| Status | Code | When |
|---|---|---|
401 | UNAUTHORIZED | The key is missing, malformed or revoked, or its owner is no longer a member of the workspace. |
402 | PAID_PLAN_REQUIRED | The workspace's plan does not include the API. The full API is included with xPlant+ Teams and Enterprise; on Hobby and Pro Lab, keys can connect devices only. |
403 | FORBIDDEN | The key lacks a scope this operation requires, or its owner's current role in the workspace cannot use it — a key never does more than its owner can in xPlant. error names the scope, and for a role, the role it needs. |
403 | DEVICE_TOKEN_NOT_ACCEPTED | A device token was sent; this operation needs a workspace API key. |
422 | VALIDATION_ERROR | Both cursor and offset were sent; use one. |
422 | INVALID_CURSOR | The cursor is malformed, or came from a different list or different filters. Start again without it. |
422 | VALIDATION_ERROR | entity is missing or is not plant or explant, or since is not a valid timestamp. |
429 | RATE_LIMIT_EXCEEDED | Too many requests for this key, device token or workspace. Wait Retry-After seconds. |
500 | EVENT_QUERY_FAILED | The history could not be read. Retry later. |
Branch on code, never on the error text. See Errors.
Record a transfer
Records a transfer — a subculture onto fresh media — for one plant or explant. Send exactly one of plant_id or explant_id. Everything else is optional: the date defaults to today and the cycle to the next one in the record's history.
List contaminations
The workspace's contamination logs, newest first. Narrow to one plant or explant, to one status, or to what was logged since a point in time. Withdrawn logs are never listed.