Skip to content

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.

  1. Get the result. A CI report answers each matched entry with its resultUlid (results[i].resultUlid of POST /api/v1/projects/{projectId}/reports). GET /api/v1/runs/{runUlid}/results lists every result of a run.
  2. Stage — POST /api/v1/runs/{runUlid}/results/{resultUlid}/attachments:stage with multipart/form-data and one or more file parts. Returns one staged ref per file. Nothing is attached yet and no storage is charged.
  3. Commit — PATCH /api/v1/runs/{runUlid}/results/{resultUlid}/attachments with the result’s complete list, each item carrying a position. Returns the committed list.
  4. Read — GET /api/v1/runs/{runUlid}/results/{resultUlid} returns resultAttachments, ordered by position: 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.

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.

Terminal window
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 file parts 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.

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.

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, and originalFilename; sending the staged ref unchanged plus position is 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 stepIndex to 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 forbidden and 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 answers 413 storage_quota_exceeded.

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 itemNew itemExisting attachment
OmittedAttached to the resultKeeps its current step
nullAttached to the resultMoves back to the result
A numberAttached to that stepMoves to that step
  • The index must name a step of this result. Otherwise the whole commit answers 422 validation_failed and nothing is written.
  • Step attachments count toward the same limit of 20 files per result.
  • The commit response, resultAttachments on the attempt snapshot and the run case detail return each attachment’s stepIndex (null for a result-level file). Resend it as is when you commit the list again, or omit it to keep it.
Terminal window
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.

StatusCodeWhen
403forbiddenviewer role; or an app token whose commit would remove an attachment, or that calls an operation other than the result-scoped stage and commit
403run_case_assignee_lockedThe project locks results to the assignee and the caller is not the assigned tester (result-scoped operations)
403project_lockedThe project is locked
404not_foundUnknown run, result, or run case, or one in another organization
409conflictThe run is aborted, or closed while allowResultsInClosedRuns is false; the run-case stage refuses every closed run
409staging_already_committedCommit only: a staged ref was already committed
409project_archivedThe 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
413storage_quota_exceededCommit only: the organization storage quota would be exceeded
415unsupported_media_typeCommit only: the body is not application/json
422validation_failedStage: 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
429too_many_requestsThe organization’s API rate limit; wait Retry-After seconds

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.