Get an SOP run
One run: the SOP and version it follows, where it stands, and the evidence posted against its steps, oldest first — the trail to read forwards when you need to know what was done, in what order.
https://app.xplantpro.com/ api/ v1/ sop-runs/ {id}read:sop_runsThe trail is paged. events holds its first 50 records, and meta.events_next_cursor says where the rest starts: pass it as cursor to List a run's evidence, and keep following meta.next_cursor there until it is null. When meta.events_next_cursor is null, events is the whole trail.
sopRuns.get() follows meta.events_next_cursor for you, so run.events is always the whole trail. For a very long run, page it with sopRuns.listEvents() instead.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | The run's id. |
Example
curl https://app.xplantpro.com/api/v1/sop-runs/2a4c6e8b-1d3f-4b5a-a7c9-e1f3a5b7c9d1 \
-H "Authorization: Bearer $XPLANT_API_KEY"Response
200 with { "ok": true, "data": … }. data holds the result.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | — |
sopId | string | null (uuid) | Yes | The SOP being run. |
version | integer | null | Yes | The version of the SOP this run follows — the one in force when it started. It never changes afterwards. |
versionId | string | null (uuid) | Yes | That version's id. |
status | string | Yes | in_progress from the moment a run starts. A run ends as completed, failed or cancelled, closed in xPlant or with Complete an SOP run; runs can also be pending, on_hold or archived. |
batchCode | string | null | Yes | Your lab's own identifier for what is being run. |
startedAt | string | null | Yes | — |
completedAt | string | null | Yes | When the run was completed. Empty while it runs, and for a run that ended failed or cancelled. |
progress | number | Yes | How far through its steps the run is, from 0 to 100, as ticked off in xPlant. |
steps | any[] | Yes | The step checklist as ticked off in xPlant, one entry per step, such as { "id": "step-3", "completed": true }. Empty until someone works the run in xPlant. Evidence posted through the API is in events and does not tick steps off here. |
events | object[] | Yes | The first page of evidence posted against the run's steps, oldest first: up to 50 records. meta.events_next_cursor continues it. |
events[].id | string (uuid) | Yes | — |
events[].runId | string (uuid) | Yes | The run the evidence belongs to. |
events[].stepKey | string | Yes | The step it was recorded against — the stepId it was posted to. |
events[].eventType | string | Yes | confirmed, scanned, skipped, note or device_state for a step event; measured for a measurement. |
events[].recordedAt | string | Yes | When it happened: the recorded_at that was sent, or when xPlant received it. |
events[].payload | object | Yes | The detail sent with the event. For a measurement: metric, value, unit, and notes when there were any. |
{
"ok": true,
"data": {
"id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"sopId": "2e4f6a8c-0b1d-4e3f-9a5b-7c9d1e3f5a7b",
"version": 2,
"versionId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"status": "in_progress",
"batchCode": "LINE-0412",
"startedAt": "2026-09-25T08:02:13.000Z",
"completedAt": null,
"progress": 0,
"steps": [],
"events": [
{
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"stepKey": "step-2",
"eventType": "measured",
"recordedAt": "2026-09-25T08:21:40+00:00",
"payload": {
"metric": "ph",
"value": 5.7,
"unit": "pH",
"notes": "Initiation medium before pouring."
}
},
{
"id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"runId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"stepKey": "step-3",
"eventType": "scanned",
"recordedAt": "2026-09-25T08:30:00+00:00",
"payload": {
"code": "LINE-0412-J07"
}
}
]
}
}The envelope can also carry meta:
| Field | Type | Required | Description |
|---|---|---|---|
events_next_cursor | string | null | Yes | Where the rest of the trail starts. Pass it unchanged as cursor to List a run's evidence for the evidence after events. null when events is the whole trail. |
| Response header | Meaning |
|---|---|
X-RateLimit-Limit | The request budget this answer reports on, per minute: your key's or device token's (1,000) or your workspace's (3,000), whichever has fewer requests left. Left out when the budget could not be read. |
X-RateLimit-Remaining | Requests left in that budget this minute, after this one. 0 on a 429. A workspace's other keys spend its budget too, so this can fall faster than your own requests alone would. |
X-RateLimit-Reset | Whole seconds until that budget's minute ends and it is full again. Never more than 60. |
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. |
404 | NOT_FOUND | No run with this id exists in the key's workspace, or the run id is not a well-formed id. |
429 | RATE_LIMIT_EXCEEDED | Too many requests for this key, device token or workspace. Wait Retry-After seconds. |
500 | SOP_RUN_QUERY_FAILED | The run could not be read. Retry later. |
Branch on code, never on the error text. See Errors.
Start an SOP run
Starts a run of an SOP: the record that a procedure was carried out, step by step, against the version the lab had in force.
Complete an SOP run
Ends a run: completed when the procedure was carried out, failed when it was carried out and did not work, cancelled when it was stopped before the end. Afterwards the run reads exactly as one closed in xPlant: its status is the outcome, and completedAt is the time of this call for completed and empty otherwise. A run marked failed raises the same deviation alert it raises in xPlant.