Connect a bucket to the project, or replace the connection
const url = 'https://beta-api.plune.ai/v1/projects/example/storage';const options = { method: 'PUT', headers: { cookie: 'plune_session=<plune_session>', 'Content-Type': 'application/json' }, body: '{"provider":"aws","bucket":"acme-test-results","region":"eu-central-1","accessKeyId":"AKIAIOSFODNN7EXAMPLE","secret":"example-not-a-real-secret"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”An id from GET /v1/projects
Request Body required
Section titled “Request Body required ”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
aws — Amazon S3; r2 — Cloudflare R2; s3 — any other S3-compatible service (MinIO and the like).
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.
Required for aws and s3; refused for r2.
Required for r2; refused for the others.
Required for s3 — an https address; refused for the others.
s3 only; default false.
Visible characters, no spaces. Kept as it is, and shown back as its last four characters only.
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.
Examples
Amazon S3
{ "provider": "aws", "bucket": "acme-test-results", "region": "eu-central-1", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "secret": "example-not-a-real-secret"}Cloudflare R2 — the account id, no region
{ "provider": "r2", "bucket": "acme-test-results", "accountId": "0123456789abcdef0123456789abcdef", "accessKeyId": "example-r2-access-key-id", "secret": "example-not-a-real-secret"}Any other S3-compatible service — an endpoint, a region, a folder of your own
{ "provider": "s3", "bucket": "acme-test-results", "endpoint": "https://minio.acme.example", "region": "us-east-1", "pathStyle": true, "prefix": "ci/plune", "accessKeyId": "example-minio-access-key", "secret": "example-not-a-real-secret"}Responses
Section titled “ Responses ”Connected — the same body as GET
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
Present exactly when configured is true, as every field below.
auto for r2.
r2 only.
s3 only.
Exactly the last four characters of the access key id: enough to tell two keys apart, not to use one.
userId of the owner who connected it.
When the SAVED connection was last checked (POST …/test with no body); null if it never was, or since it last changed.
What that check found; null when verifiedAt is.
object
object
Present exactly when result is failed.
The HTTP status the bucket answered with, when it answered and the step did not want that. Never its body.
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
object
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"}A field the provider has no use for
{ "error": "validation failed — accountId: is not used with aws — leave it out"}No / invalid session — a CLI token is refused here
You are in the project, but not as its owner
object
Example
{ "error": "project owner only"}No such project among yours
object
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.
object
Example generated
{ "error": "example"}Headers
Section titled “Headers ”Seconds until the window rolls over
The server has no key to keep Secrets under; nothing is saved
object
Example
{ "error": "storage is not configured on this server — contact your operator"}