Attach a media file
Attaches an image to a plant, explant, contamination log or SOP in the workspace. It appears on that record in xPlant exactly like a photo added in the app. Send the image as image_url or as image_base64 — exactly one.
https://app.xplantpro.com/ api/ v1/ assetswrite:assetsHonours Idempotency-KeyLimits. JPEG, PNG, WebP or GIF, up to 10 MB. The type is read from the image itself, not from its name. A request body can be at most about 4.5 MB, and base64 makes a file about a third larger, so send image_base64 for images up to about 3 MB and image_url for anything larger. A request body over that size is refused with 413 before it reaches the API.
image_url. xPlant downloads the image once, over https, from the public internet. Addresses on private or local networks are refused, redirects are not followed, and the download must finish within 10 seconds.
The response's view_url works for 15 minutes. A retry with the same Idempotency-Key returns the first response, with a fresh view_url.
Send an Idempotency-Key header to make retries safe: a repeat with the same key within 24 hours returns the first response instead of writing twice. See Idempotency.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | No | Any unique string you choose per logical write. A retry carrying the same key within 24 hours returns the first result instead of writing again. Scoped to your API key and this operation. 8–255 characters. Must match ^[A-Za-z0-9._:~-]+$. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
target | "plant" | "explant" | "contamination" | "sop" | Yes | The kind of record to attach the image to: plant, explant, contamination, sop. |
target_id | string (uuid) | Yes | The id of that record. It must be in the key's workspace. |
image_url | string (uri) | No | An https address xPlant downloads the image from, up to 10 MB. It must be reachable from the public internet: private and local network addresses are refused, and redirects are not followed, so send the image's final address. The download must finish within 10 seconds. Use this for anything larger than about 3 MB. Up to 2048 characters. |
image_base64 | string | No | The image itself, base64-encoded, without a data: prefix. A request body can be at most about 4.5 MB and base64 makes a file about a third larger, so this suits images up to about 3 MB. Send anything larger, up to the 10 MB limit, as image_url. |
filename | string | No | The name to show for the file, for example leaf-sample-3.jpg. Defaults to the last part of image_url, or photo for an inline image. 1–200 characters. |
caption | string | No | A short note stored with the image and shown beside it in xPlant. 1–500 characters. |
Example
curl -X POST https://app.xplantpro.com/api/v1/assets \
-H "Authorization: Bearer $XPLANT_API_KEY" \
-H "Idempotency-Key: camera-2-line-0412-photo-1" \
-H "Content-Type: application/json" \
-d '{
"target": "explant",
"target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
"image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
"filename": "vessel-12-week-3.png",
"caption": "Callus forming at the cut edge"
}'Response
201 with { "ok": true, "data": … }. data holds the result.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | — |
target | "plant" | "explant" | "contamination" | "sop" | Yes | The kind of record the file is attached to. |
target_id | string (uuid) | Yes | The id of that record. |
kind | string | Yes | What the file is: photo, video, annotation, document, diagram, scan, audio, other. Images attached through the API are photo. |
file_name | string | null | Yes | The file's name as xPlant shows it. |
content_type | string | null | Yes | The file's media type, for example image/jpeg, image/png. |
caption | string | null | Yes | The note stored with the file. |
captured_at | string | null | Yes | When the photo was taken, where that was recorded. ISO 8601. |
uploaded_by | string | null | Yes | User id of the workspace member who added the file. |
created_at | string | null | Yes | When the file was added. ISO 8601. |
view_url | string | null | Yes | A link to the file itself, valid for 15 minutes from this response. Fetch the asset again for a fresh link, and never store one. null when the file cannot be linked. |
view_url_expires_at | string | null | Yes | When view_url stops working. ISO 8601. |
{
"ok": true,
"data": {
"id": "8c2f1a6e-4b3d-4f7a-9e21-5d6c7b8a9f10",
"target": "explant",
"target_id": "3f9a2c1e-7b4d-4e8f-a6c5-1d2e3f4a5b6c",
"kind": "photo",
"file_name": "vessel-12-week-3.png",
"content_type": "image/png",
"caption": "Callus forming at the cut edge",
"captured_at": null,
"uploaded_by": "0d7e4b9a-6f21-4c3e-8a5b-2e9f1c7d4a60",
"created_at": "2026-09-25T14:20:11.000Z",
"view_url": "https://files.example.com/vessel-12-week-3.png?signature=4f9c2e",
"view_url_expires_at": "2026-09-25T14:35:11.000Z"
}
}| Response header | Meaning |
|---|---|
Idempotent-Replay | true when this response is a replay of an earlier request with the same Idempotency-Key. |
X-Request-Id | Identifies this request. Include it when you contact support. |
Errors
| Status | Code | When |
|---|---|---|
400 | VALIDATION_ERROR | The request body is not valid JSON. |
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. |
409 | IDEMPOTENCY_IN_FLIGHT | A request with this Idempotency-Key is still being processed; retry after Retry-After seconds. |
413 | PAYLOAD_TOO_LARGE | The image is larger than 10 MB. |
415 | UNSUPPORTED_MEDIA_TYPE | The file is not a JPEG, PNG, WebP or GIF image. The type is read from the file itself. |
422 | VALIDATION_ERROR | The Idempotency-Key header is malformed. |
422 | VALIDATION_ERROR | A field failed validation. error names the first one, for example title: title is required. |
422 | VALIDATION_ERROR | target_id does not name a record of that kind in the key's workspace. |
422 | VALIDATION_ERROR | image_url is not an https address on the public internet. |
422 | IMAGE_URL_FETCH_FAILED | image_url could not be downloaded: it redirected, answered with an error status, took longer than 10 seconds, or its host could not be found. error says which. |
429 | RATE_LIMIT_EXCEEDED | Too many requests for this key, device token or workspace. Wait Retry-After seconds. |
500 | ASSET_UPLOAD_FAILED | The image could not be stored. Retry with the same Idempotency-Key. |
500 | ASSET_INSERT_FAILED | The image could not be saved to the record. Retry with the same Idempotency-Key. |
Branch on code, never on the error text. See Errors.
List media files
The photos and files attached to one plant, explant, contamination log or SOP, newest first. Each carries a view_url: a link to the file itself that works for 15 minutes. List again for fresh links, and never store one.
Get a media file
One photo or file, with a view_url that works for 15 minutes from this response. Fetch it again whenever you need the file; never store the link.