Result attachments
A result (one attempt of a run case) can carry up to 20 files of almost any type: screenshots, Playwright traces, videos, logs. Upload them in two steps: stage the files, then commit the result’s attachment list. An attachment belongs to the whole result or, with stepIndex, to one of the steps a CI report sent with it (Attach to a reported step); for the images a tester adds to a case step while executing, see Run result attachments.
Access requires member+ on the run’s project; a viewer receives 403 forbidden. Writes follow the same closed-run rule as the other result writes: allowed on an open run, and on a closed, non-aborted run while the project’s allowResultsInClosedRuns run setting resolves to true (the default). An aborted run always refuses with 409 conflict. When the project locks results to the assignee (assigneeResultLock), a caller who is not the assigned tester receives 403 run_case_assignee_locked; API tokens are never exempt.
- Get the result. A CI report answers each matched entry with its
resultUlid(results[i].resultUlidofPOST /api/v1/projects/{projectId}/reports).GET /api/v1/runs/{runUlid}/resultslists every result of a run. - Stage —
POST /api/v1/runs/{runUlid}/results/{resultUlid}/attachments:stagewithmultipart/form-dataand one or morefileparts. Returns one staged ref per file. Nothing is attached yet and no storage is charged. - Commit —
PATCH /api/v1/runs/{runUlid}/results/{resultUlid}/attachmentswith the result’s complete list, each item carrying aposition. Returns the committed list. - Read —
GET /api/v1/runs/{runUlid}/results/{resultUlid}returnsresultAttachments, ordered byposition: the committed files, and the images of the result’s texts (see Images in the text).
A staged file that is never committed expires on its own (about 48 hours). Retrying a stage request is therefore safe; stage requests ignore the Idempotency-Key header. The commit and the delete accept it.
Stage (multipart)
Section titled “Stage (multipart)”Keep the whole request body at most 64 MiB (ATTACHMENT_UPLOAD_REQUEST_RECOMMENDED_MAX_BYTES) and split larger batches across requests. Cloudflare refuses a request body over 100 MB with 413 before it reaches the API, whatever the per-file and per-request limits allow; that 413 does not carry the API error envelope.
curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -F "file=@screenshot.png" \ -F "file=@trace.zip" \ "https://app.probara.net/api/v1/runs/$RUN/results/$RESULT/attachments:stage"{ "attachments": [ { "ulid": "01J...ATT1", "objectKey": "staging/01J...ORG/01J...UPLOAD1.webp", "thumbKey": "staging/01J...ORG/01J...UPLOAD1_thumb.webp", "mime": "image/webp", "byteSize": 8420, "originalFilename": "screenshot.png", "disposition": "inline", "width": 1280, "height": 720 }, { "ulid": "01J...ATT2", "objectKey": "staging/01J...ORG/01J...UPLOAD2.zip", "thumbKey": null, "mime": "application/zip", "byteSize": 1048576, "originalFilename": "trace.zip", "disposition": "attachment", "width": null, "height": null } ]}Limits:
- Up to 20
fileparts per request. Empty files are refused. - Any type except executables and scripts (
application/x-msdownload,application/x-msdos-program,application/x-sh,application/x-bat,application/x-msi,application/x-executable,application/vnd.microsoft.portable-executable), checked against each part’s declared content type. - PNG, JPEG, and WebP images (by declared content type) are converted to WebP with a thumbnail and come back with
disposition: "inline". They are limited to 10 MiB and 8192 px per side. - Every other file is stored as sent, up to 32 MiB, with
disposition: "attachment".
The first invalid part refuses the whole request with 422 validation_failed: no refs come back, not even for the parts before it (those expire like any other unreferenced staged file). The error message is empty_file, denied_type, or file_too_large, which also covers an image over the pixel limit or whose header cannot be read. Validate files before uploading, or retry the remaining files one per request so one bad file does not hold back the others.
Before the result exists
Section titled “Before the result exists”A client that uploads evidence before it records the result can stage against the run case instead: POST /api/v1/runs/{runUlid}/cases/{runCaseUlid}/result-attachments:stage. It takes the same body, has the same limits, and returns the same refs. It refuses any closed run with 409 conflict, whatever the settings. Commit the refs to the result once it exists.
Commit (replace the list)
Section titled “Commit (replace the list)”The body is the result’s complete attachment list. Committed files left out are deleted, so resend each existing file as { ulid, position } to keep it. A new item is a staged ref plus its position.
{ "attachments": [ { "ulid": "01J...KEEP", "position": 0 }, { "ulid": "01J...ATT2", "position": 1, "objectKey": "staging/01J...ORG/01J...UPLOAD2.zip", "mime": "application/zip", "byteSize": 1048576, "originalFilename": "trace.zip", "disposition": "attachment", "thumbKey": null, "width": null, "height": null } ]}- A new item needs at least
ulid,position,objectKey, andoriginalFilename; sending the staged ref unchanged pluspositionis simplest. - A result holds at most 20 attachments. The commit is all-or-nothing: a refused commit changes nothing.
{ "attachments": [] }removes every attachment of the result.- An item may carry
stepIndexto attach the file to a reported step; see below. - An app token may only add: a body that leaves out an attachment the result already has answers
403 forbiddenand changes nothing. An app token can neither stage against the run case nor delete an attachment. - Committed bytes count toward the organization storage quota (category
result_attachment); staged bytes do not. A commit whose growth would exceed the quota answers413 storage_quota_exceeded.
Attach to a reported step
Section titled “Attach to a reported step”A result recorded by a CI report can carry the steps the
execution ran (the entry’s steps). To attach a file to one of them, give the item stepIndex: the
step’s index in a depth-first, pre-order walk of the steps as they were sent, from 0 — the step’s
position in resultSteps on the attempt snapshot.
{ "attachments": [ { "ulid": "01J...KEEP", "position": 0 }, { "ulid": "01J...SHOT", "position": 1, "stepIndex": 3, "objectKey": "staging/01J...ORG/01J...UPLOAD3.webp", "originalFilename": "pay.png" } ]}stepIndex on the item | New item | Existing attachment |
|---|---|---|
| Omitted | Attached to the result | Keeps its current step |
null | Attached to the result | Moves back to the result |
| A number | Attached to that step | Moves to that step |
- The index must name a step of this result. Otherwise the whole commit answers
422 validation_failedand nothing is written. - Step attachments count toward the same limit of 20 files per result.
- The commit response,
resultAttachmentson the attempt snapshot and the run case detail return each attachment’sstepIndex(nullfor a result-level file). Resend it as is when you commit the list again, or omit it to keep it.
Delete one attachment
Section titled “Delete one attachment”curl -sS -X DELETE \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/runs/$RUN/results/$RESULT/attachments/$ATT"204 on success. Deleting an attachment that is already gone also answers 204.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 403 | forbidden | viewer role; or an app token whose commit would remove an attachment, or that calls an operation other than the result-scoped stage and commit |
| 403 | run_case_assignee_locked | The project locks results to the assignee and the caller is not the assigned tester (result-scoped operations) |
| 403 | project_locked | The project is locked |
| 404 | not_found | Unknown run, result, or run case, or one in another organization |
| 409 | conflict | The run is aborted, or closed while allowResultsInClosedRuns is false; the run-case stage refuses every closed run |
| 409 | staging_already_committed | Commit only: a staged ref was already committed |
| 409 | project_archived | The project is archived |
| 413 | — | Stage only: the request body is over 100 MB; Cloudflare refuses it before it reaches the API, without the error envelope |
| 413 | storage_quota_exceeded | Commit only: the organization storage quota would be exceeded |
| 415 | unsupported_media_type | Commit only: the body is not application/json |
| 422 | validation_failed | Stage: no file part, too many files, an empty or oversized file, an image over the pixel limit, or a denied type; one invalid part refuses the whole request. Commit: an invalid body, more than 20 attachments, an invalid or expired staged ref, or a stepIndex that names no step of the result |
| 429 | too_many_requests | The organization’s API rate limit; wait Retry-After seconds |
Images in the text
Section titled “Images in the text”The resultAttachments list also holds the images that sit inline in the result’s texts, next to its files. Each image has kind image and the fields of an upload (ulid, mime, width, height, byteSize, createdAt), plus field, which names the text it sits in:
{ "kind": "description" },{ "kind": "precondition" }or{ "kind": "postcondition" }for the case texts the attempt froze with its header.{ "kind": "step_actual_result", "runCaseStepUlid": "01J...", "position": 2 }for the actual result of a step.{ "kind": "notes" }for the result notes.
Files keep kind file. An image leaves the list when it leaves its text.
Related
Section titled “Related”- Step actual results — the text and inline images of a step’s result
- Test-run settings —
allowResultsInClosedRunsandassigneeResultLock - Interactive v1 reference — full schemas