Skip to content

Announce files for a result — step 1 of 3 of sending them to the project’s bucket

POST
/v1/results/{id}/files/uploads
curl --request POST \
--url https://beta-api.plune.ai/v1/results/example/files/uploads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "files": [ { "name": "signed-in.png", "contentType": "image/png", "size": 48213 }, { "name": "trace.zip", "contentType": "application/zip", "size": 3145728 } ] }'

Files that are not screenshots or small texts — a trace, a video, a log of megabytes — go straight to the bucket the project connected (ADR 0041); Plune never carries their bytes. Three HTTP calls are all a reporter needs, in any language, with no S3 library and no key of the bucket:

  1. POST /v1/results/{id}/files/uploads (this call) with { "files": [{ "name", "contentType", "size" }] }, size being the exact number of bytes. The answer has one entry per file, in the same order: { id, name, url, method: "PUT", headers, expiresAt } for a file that goes to the bucket (ResultFileUploadLink); { name, direct: true } for a picture or a text of a project without a bucket, which goes to POST /v1/results/{id}/files?name= as before; { name, skipped: "no_storage" } for anything else of a project without a bucket — nothing to send, and not an error.
  2. PUT the bytes to url, to the bucket and not to Plune: with exactly headers (content-type and content-length) and no Authorization header, before expiresAt — 15 minutes. A 200 means the bucket took it; anything else means the file is not there, and a new link is a new call to this route. The link is signed for that type and that size: a different body is refused by the bucket.
  3. POST /v1/files/{id}/complete with the id of the entry, no body. Plune looks in the bucket, and only a file that is there with the size and type announced becomes part of the result.

The ceilings of a file in the bucket, which are checked here by the size you announce and again in step 3 by the size found: image 10 MiB, text 5 MiB, video 100 MiB, trace 100 MiB, other 50 MiB. The kind is read from contentType and the name (an image type; JSON, .log, stdout, stderr, plain text or .md; a video type or .webm/.mp4; trace or trace*.zip; anything else). A result takes 30 files, the database and the bucket together. Nothing is shown until step 3: a file that was announced and never completed is not in any list, and is removed with its object after an hour. Into a run that is still open, for a result of the caller’s project: someone else’s result is a 404, the same as an invented one. A reader of the project cannot announce files. 3000 requests a minute a person, for each of the three routes a result’s files go through. A link is never written to any log by Plune; do not write it to yours.

id
required
string

The result — items[].id of the batch that stored it.

Media type application/json

The files a client is about to send to a result, as it names them. Only these three fields are read.

object
files
required
Array<object>
>= 1 items
object
name
required

What the file is called; only the last segment of a path is kept, cleaned of credentials. At most 200 characters once cleaned.

string
contentType
required

The media type the file will be sent with: type/subtype, optionally ; name=value parameters, no comma. The link is signed for exactly this.

string
size
required

The exact number of bytes of the body. The link is signed for exactly this, and complete checks it in the bucket.

integer
>= 1
Example
{
"files": [
{
"name": "signed-in.png",
"contentType": "image/png",
"size": 48213
},
{
"name": "trace.zip",
"contentType": "application/zip",
"size": 3145728
}
]
}

One entry per file. Shown: the two files of the request, for a project with a bucket (bucket) and without one (noBucket)

Media type application/json
object
files
required

One entry for each requested file, in the order they were requested.

Array
One of:

The project has a bucket: this file goes there. PUT the bytes to url with headers, then POST /v1/files/{id}/complete.

object
id
required

The id of the file: what complete takes, and what the file is called in every list from then on.

string
name
required

The name as it was cleaned and will be shown.

string
url
required

A signed address in the customer’s bucket. It is a right, not a name: whoever holds it can write that one object until it expires. Do not print it or log it.

string
method
required
string
Allowed values: PUT
headers
required

Exactly the headers to send, lower-case names: content-type, and content-length — the length of the body, which an HTTP client that sets it from the body itself sends already. Nothing else is signed; add no Authorization.

object
key
additional properties
string
expiresAt
required

The link works for 15 minutes from the answer.

string format: date-time
Examples

The project has a bucket

{
"files": [
{
"id": "3f0c2a7e-1b5d-4c8e-9a61-2d7f4e8b0c13",
"name": "signed-in.png",
"url": "https://s3.eu-central-1.amazonaws.com/acme-results/plune/u-shop%3A%3Adefault/run/result/3f0c2a7e-1b5d-4c8e-9a61-2d7f4e8b0c13-signed-in.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...&X-Amz-Date=20261010T090000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost&X-Amz-Signature=…",
"method": "PUT",
"headers": {
"content-type": "image/png",
"content-length": "48213"
},
"expiresAt": "2026-10-10T09:15:00.000Z"
},
{
"id": "3f0c2a7e-1b5d-4c8e-9a61-2d7f4e8b0c14",
"name": "trace.zip",
"url": "https://s3.eu-central-1.amazonaws.com/acme-results/plune/…?X-Amz-Signature=…",
"method": "PUT",
"headers": {
"content-type": "application/zip",
"content-length": "3145728"
},
"expiresAt": "2026-10-10T09:15:00.000Z"
}
]
}

A body that is not { files: [{ name, contentType, size }] } — no files, a size that is not a whole number of bytes from 1, a name that is empty or over 200 characters once cleaned, or a contentType that is not a media type

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

No / invalid token or session

A reader of the project

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Not found or not yours

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

The run is closed; or the result would hold more than 30 files (code: too_many_files)

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

A file over the ceiling of its kind in the bucket. The body names the ceiling: { error, code: "file_too_large", limit: "image" | "text" | "video" | "trace" | "other", maxBytes }. Nothing is reserved for any file of the request

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Rate-limited: the per-route throttle, or the per-minute request limit of the plan an API token counts by (#1442; the body names the plan, #2092). Retry-After carries the seconds until the window rolls over.

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}
Retry-After
integer

Seconds until the window rolls over

The project has a bucket and the server cannot open its key (code: platform_not_configured)

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}