Skip to content

Connect a bucket to the project, or replace the connection

PUT
/v1/projects/{id}/storage
curl --request PUT \
--url https://beta-api.plune.ai/v1/projects/example/storage \
--header 'Content-Type: application/json' \
--cookie plune_session=<plune_session> \
--data '{ "provider": "aws", "bucket": "acme-test-results", "region": "eu-central-1", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "secret": "example-not-a-real-secret" }'

Owner only. Saves the bucket and its key — the Secret is encrypted with the server’s key, bound to this project, and is never returned, in whole or in part — and answers with what GET shows. It makes no request to the bucket: POST …/test does. Replacing a connection keeps the Secret already saved only when the provider, the account, the endpoint and the access key id are the same ones; for any other change secret is required, so a connection cannot be pointed at another account with the old key. Saving resets the last check: verifiedAt and verifyResult are null until the connection is checked again. The address — bucket, region, accountId, endpoint — is judged before anything is saved, and a refusal names the field, never the value. Recorded in the audit feed of the project as a storage_settings entry, with the access key id and the Secret masked. Session cookie only.

id
required
string

An id from GET /v1/projects

Media type application/json

A bucket, and the key that opens it. The fields a provider needs differ, and one it has no use for is refused by name: aws takes region; r2 takes accountId (the region is always auto, so a region is refused); s3 takes endpoint and region, and pathStyle for a service that wants <host>/<bucket>/<key>. The bucket, the region, the accountId and the endpoint are judged by the rules for an address before anything is saved or any request is made; a refusal names the field and never what was written in it.

object
provider
required

aws — Amazon S3; r2 — Cloudflare R2; s3 — any other S3-compatible service (MinIO and the like).

string
Allowed values: aws r2 s3
bucket
required
string
>= 1 characters <= 255 characters
prefix

The folder in the bucket the files go under: folder names separated by /, made of letters, digits and . _ -; no empty folder, no . or .. folder, no / at the start or the end.

string
default: plune <= 120 characters /^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$/
region

Required for aws and s3; refused for r2.

string
>= 1 characters <= 64 characters
accountId

Required for r2; refused for the others.

string
>= 1 characters <= 64 characters
endpoint

Required for s3 — an https address; refused for the others.

string
>= 1 characters <= 2048 characters
pathStyle

s3 only; default false.

boolean
accessKeyId
required

Visible characters, no spaces. Kept as it is, and shown back as its last four characters only.

string
>= 8 characters <= 256 characters /^[\x21-\x7e]{8,256}$/
secret

Write-only: stored encrypted, bound to the project, and in no response, no log and no error message. Required for a new connection, and whenever the provider, the accountId, the endpoint or the accessKeyId is not the saved one; when only the bucket, the prefix, the region or pathStyle change, leaving it out keeps the saved one.

string
>= 1 characters <= 1024 characters /^[\x21-\x7e]{1,1024}$/
Examples

Amazon S3

{
"provider": "aws",
"bucket": "acme-test-results",
"region": "eu-central-1",
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
"secret": "example-not-a-real-secret"
}

Connected — the same body as GET

Media type application/json

The bucket of a project, as every member of it reads it. { "configured": false } when none is connected. The Secret is not in it, nor is the whole access key id.

object
configured
required
boolean
provider

Present exactly when configured is true, as every field below.

string
Allowed values: aws r2 s3
bucket
string
prefix
string
region

auto for r2.

string
accountId

r2 only.

string
endpoint

s3 only.

string
pathStyle
boolean
accessKeyLast4

Exactly the last four characters of the access key id: enough to tell two keys apart, not to use one.

string
>= 4 characters <= 4 characters
savedBy

userId of the owner who connected it.

string
savedAt
string format: date-time
verifiedAt

When the SAVED connection was last checked (POST …/test with no body); null if it never was, or since it last changed.

string format: date-time
nullable
verifyResult

What that check found; null when verifiedAt is.

object
ok
required
boolean
steps
required
Array<object>
object
step
required
string
Allowed values: reach write read cors public delete
result
required
string
Allowed values: ok failed skipped
code

Present exactly when result is failed.

string
Allowed values: unreachable tls bad_credentials no_bucket wrong_region write_denied read_denied delete_denied cors_missing unexpected
status

The HTTP status the bucket answered with, when it answered and the step did not want that. Never its body.

integer
warnings
required
Array<string>
Allowed values: bucket_public
Example
{
"configured": true,
"provider": "aws",
"bucket": "acme-test-results",
"prefix": "plune",
"region": "eu-central-1",
"pathStyle": false,
"accessKeyLast4": "MPLE",
"savedBy": "u-shop",
"savedAt": "2026-09-16T09:12:04.151Z",
"verifiedAt": null,
"verifyResult": null
}

Invalid body — a field that is missing, wrong, not used by the provider, or not an address a bucket can have; the message names the field and never a value

Media type application/json
object
error
required
string
Examples

A connection that changed, with no Secret to go with it

{
"error": "validation failed — secret: is required for a new connection, and whenever the provider, the account, the endpoint or the access key id changes"
}

No / invalid session — a CLI token is refused here

You are in the project, but not as its owner

Media type application/json
object
error
required
string
Example
{
"error": "project owner only"
}

No such project among yours

Media type application/json
object
error
required
string
Example
{
"error": "project 'u-shop::default' not found"
}

Rate-limited: either the per-route throttle or a tenant quota. 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 server has no key to keep Secrets under; nothing is saved

Media type application/json
object
error
required
string
Example
{
"error": "storage is not configured on this server — contact your operator"
}