bucketpilot/Docs

Buckets & objects

List buckets and search their indexed objects.

List buckets

GET /v1/buckets returns every bucket in the key's workspace:

bash
curl https://bucketpilot.io/api/v1/buckets \
  -H "Authorization: Bearer $BUCKETPILOT_KEY"
json
{
  "buckets": [
    {
      "id": 12,
      "name": "prod-data",
      "awsBucketName": "acme-prod-data",
      "region": "us-east-1",
      "provider": "aws",
      "objectCount": 48210,
      "totalSizeBytes": 734003200000,
      "indexStatus": "ready",
      "indexedObjectCount": 48210,
      "lastIndexedAt": "2026-07-15T04:12:09.000Z",
      "createdAt": "2026-03-02T18:30:11.000Z"
    }
  ]
}

GET /v1/buckets/{id} returns one bucket in the same shape. provider is the hosting cloud — aws, r2 (Cloudflare R2), or gcs (Google Cloud Storage); R2 buckets report region auto.

Search objects

GET /v1/buckets/{id}/objects reads the search index (metadata only — key, size, storage class; never file contents), so the bucket must be indexed first. Results are paged.

ParameterTypeDescription
searchstringText match on the key; * and ? work as wildcards (reports/*.csv).
prefixstringLimit to keys under a prefix.
storageClassstringe.g. STANDARD, GLACIER, DEEP_ARCHIVE.
page, pageSizeintegerPagination — default 50, max 200 per page.
sortBystringkey, size, or lastModified.
sortDirstringasc or desc.
bash
curl "https://bucketpilot.io/api/v1/buckets/12/objects?search=*.parquet&sortBy=size&sortDir=desc" \
  -H "Authorization: Bearer $BUCKETPILOT_KEY"
python
import requests

resp = requests.get(
    "https://bucketpilot.io/api/v1/buckets/12/objects",
    params={"search": "*.parquet", "sortBy": "size", "sortDir": "desc"},
    headers={"Authorization": f"Bearer {KEY}"},
)
for obj in resp.json()["objects"]:
    print(obj["key"], obj["size"])
json
{
  "objects": [
    {
      "key": "events/2026/06/day-30.parquet",
      "size": 52428800,
      "lastModified": "2026-07-01T00:14:55.000Z",
      "etag": "\"9b2cf535f27731c974343645a3985328\"",
      "storageClass": "STANDARD",
      "contentType": "application/octet-stream"
    }
  ],
  "page": 1,
  "pageSize": 50,
  "totalCount": 1284
}

Download objects

POST /v1/buckets/{id}/objects/download returns a short-lived presigned URL — your download comes straight from your own S3, BucketPilot never proxies the bytes. Send the object key in the body; expiresIn (seconds, default 3600) and versionId are optional.

⚠️ A presigned URL cannot outlive the credentials that signed it. The protocol allows up to seven days, and a bucket connected with access keys can use all of it — but a bucket connected through a role is signed with a temporary session, and the link stops working when that session ends, whatever you asked for. There is no error when it is created; it simply fails the first time someone uses it. So expiresIn is clamped server-side, and the response tells you the real ceiling in maxExpiresIn, what imposed it in limitedBy (session or sigv4), and why in limitReason. Read those rather than assuming the number you sent.

bash
curl -X POST https://bucketpilot.io/api/v1/buckets/12/objects/download \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "reports/2026/june.csv", "expiresIn": 600 }'
json
{
  "downloadUrl": "https://acme-prod-data.s3.us-east-1.amazonaws.com/reports/2026/june.csv?X-Amz-...",
  "key": "reports/2026/june.csv",
  "expiresAt": "2026-07-16T10:24:03.000Z",
  "maxExpiresIn": 3540,
  "limitedBy": "session",
  "limitReason": "This bucket is connected through a role, and a link can only outlive the role session that signed it…",
  "anyoneWithTheLink": true
}

The URL carries Content-Disposition: attachment, so fetching it saves the file rather than rendering it. And anyoneWithTheLink is always true, stated so nothing has to infer it: the signature is the authorisation — whoever holds the URL can download the object until it expires, with no sign-in and no way to revoke it short of deleting the object or rotating the credential.

Accept uploads from your own users

POST /v1/buckets/{id}/objects/upload-post returns a signed form — an action URL and a set of hidden fields — that a browser can post a file to directly. It is the thing a presigned PUT cannot do: a PUT URL is minted per file by something that already knows what the file is, while a POST policy is a small signed contract for a file nobody has chosen yet. Your users upload straight into your bucket; the bytes never touch your server, and your server never holds an S3 key.

bash
curl -X POST https://bucketpilot.io/api/v1/buckets/12/objects/upload-post \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "keyPrefix": "incoming/", "contentTypePrefix": "image/", "maxBytes": 5242880, "expiresIn": 600 }'

Post the returned fields and then the file, in that order, as multipart/form-data to url. Send each field once — S3 refuses a form carrying two key parameters. The response's postAs object repeats these rules alongside the policy so your code need not carry them.

A policy is a bearer capability — whoever holds it can upload until it expires, with no further identity check. So every one is scoped before it is signed:

  • •Where. key for one exact object, or keyPrefix for a folder. An empty keyPrefix is refused: it would authorise a write anywhere in the bucket, which is the same as handing over a key.
  • •What the object gets called. Post key back exactly as it was given to you. With a keyPrefix the value is a ${filename} template, and S3 replaces it with the name of the file that was uploaded — so the object lands under your prefix, named after the file. Overwriting key with a name of your own is refused (AccessDenied … Policy Condition failed: ["eq", "$key", …]): the policy is bound to that value. If you need to choose the name, ask for a policy with an exact key instead.
  • •How big. maxBytes becomes a content-length-range condition and defaults to 25 MB. S3 makes this optional; BucketPilot does not, because without it the holder can put 5 TB in your bucket at your expense.
  • •What type. contentType for one exact type, or contentTypePrefix for a family such as image/. With a prefix, your form must include a Content-Type field — an upload that omits it is refused, as is one outside the family.
  • •How long. Minutes, not days — capped at 12 hours and, on a role-connected bucket, at the signing session's own life.

⚠️ Objects uploaded this way are not indexed. The browser posts straight to your provider and BucketPilot never sees it, so nothing appears in search and no watched-folder notification fires until the next index run. Call POST /v1/buckets/{id}/objects/confirm with the key once your own upload succeeds if you want it visible immediately. The response says so in indexed: false.

Needs a read_write key: it hands out the ability to write.

Trigger indexing

Needs a read/write key. POST /v1/buckets/{id}/index starts (or resumes) building the search index — the same operation as Index this bucket in the app, with the same guardrails: it returns 403 INDEX_LIMIT_REACHED if your plan's indexed-object allowance is used up, 409 LARGE_BUCKET for very large scan-mode buckets (pass ?force=true to scan anyway, or switch the bucket to daily inventory indexing in the app), and 409 if an index job is already running. Add ?fresh=true to rebuild from scratch instead of resuming a checkpoint.

bash
curl -X POST "https://bucketpilot.io/api/v1/buckets/12/index" \
  -H "Authorization: Bearer $BUCKETPILOT_KEY"

Returns 202 { "status": "indexing" }; poll GET /v1/buckets/{id} for indexStatus to become ready.

Object metadata, tags & versions

Read a single object's details live from your storage — no index needed:

  • •GET /v1/buckets/{id}/objects/metadata?key=… — size, content type, etag, last-modified, storage class, version id, and any user metadata. Add &versionId=… for a specific version.
  • •GET /v1/buckets/{id}/objects/tags?key=… — the object's tag set.
  • •GET /v1/buckets/{id}/objects/versions?prefix=… — object versions and delete markers (pass maxKeys ≤ 1000, and keyMarker/versionIdMarker from the previous page to continue).
bash
curl "https://bucketpilot.io/api/v1/buckets/12/objects/metadata?key=reports/2026/june.csv" \
  -H "Authorization: Bearer $BUCKETPILOT_KEY"

Upload objects (read/write key)

Uploading is two steps, so the bytes go straight to your storage and never through BucketPilot. Needs a read/write key.

  1. 1.POST /v1/buckets/{id}/objects/upload with the target key (and optional contentType) returns a short-lived presigned uploadUrl.
  2. 2.PUT your file to that URL.
  3. 3.POST /v1/buckets/{id}/objects/confirm with the same key so BucketPilot indexes it — now it shows up in listings and cost.
bash
# 1. get a presigned upload URL
UP=$(curl -s -X POST https://bucketpilot.io/api/v1/buckets/12/objects/upload \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" \
  -d '{ "key": "reports/2026/july.csv", "contentType": "text/csv" }')

# 2. PUT the bytes to the returned uploadUrl
curl -X PUT -T ./july.csv -H "Content-Type: text/csv" "$(echo "$UP" | jq -r .uploadUrl)"

# 3. confirm so it's indexed
curl -X POST https://bucketpilot.io/api/v1/buckets/12/objects/confirm \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" \
  -d '{ "key": "reports/2026/july.csv" }'

Delete, copy, folders, tags & storage class (read/write key)

All need a read/write key. Each keeps the search index in sync automatically.

  • •Delete — POST /v1/buckets/{id}/objects/delete with { "keys": ["a.txt", "b/c.txt"] } (up to 1000). Returns { deleted, errors }.
  • •Copy / move — POST /v1/buckets/{id}/objects/copy with { "sourceKey", "destinationKey", "move": false } (same bucket; set move: true to delete the source).
  • •Create folder — POST /v1/buckets/{id}/folders with { "name": "invoices", "prefix": "2026/" }.
  • •Set tags — PUT /v1/buckets/{id}/objects/tags with { "key", "tags": [{ "key": "team", "value": "finance" }] } (replaces the whole set).
  • •Change storage class — PUT /v1/buckets/{id}/objects/storage-class with { "key", "storageClass": "GLACIER_IR" } (STANDARD, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, GLACIER, GLACIER_IR, DEEP_ARCHIVE, REDUCED_REDUNDANCY).
bash
curl -X POST https://bucketpilot.io/api/v1/buckets/12/objects/delete \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" \
  -d '{ "keys": ["tmp/old.log", "tmp/older.log"] }'

A read-only key on any of these returns 403 READ_ONLY_KEY.

Encryption and Object Lock (read/write key to change)

The same controls as the bucket's Security tab, with the same safeguards.

  • •Read — GET /v1/buckets/{id}/security returns default encryption, versioning, Object Lock (and its default retention), MFA Delete and public-access state.
  • •Default encryption — PUT /v1/buckets/{id}/encryption with { "type": "SSE-S3" } or { "type": "SSE-KMS", "kmsKeyId": "alias/my-key", "bucketKeyEnabled": true }. A KMS key is tested with a real write before it is saved (S3 itself accepts a typo and then every upload fails); the answer carries keyCheck. Send "skipKeyCheck": true to save without the test.
  • •Object Lock on the bucket — PUT /v1/buckets/{id}/object-lock with { "enable": true, "acknowledgeIrreversible": true } (the bucket must have versioning on, and neither can ever be switched off), or { "default": { "mode": "GOVERNANCE", "days": 30 } } to set the default retention ("default": null removes it). A Compliance default also needs acknowledgeIrreversible: true.
  • •One file — GET /v1/buckets/{id}/objects/lock?key=… reads its retention and legal hold. PUT the same path with { "key", "versionId"?, "retention": { "mode": "GOVERNANCE", "retainUntil": "2027-01-01T00:00:00Z" }, "legalHold": "ON" }. A lock is only ever extended — a shorter date answers 409 LOCK_CANNOT_BE_SHORTENED. A Compliance lock needs acknowledgeIrreversible: true.

These need the extra S3 permissions listed under Connecting your AWS account; without them the answer is 403 with the permission to add.

Copy or move files, folders or a whole bucket (read/write key)

POST /v1/buckets/{id}/objects/transfer starts a transfer job and answers straight away; poll GET /v1/buckets/{id}/transfers/{jobId} until it finishes.

bash
curl -X POST https://bucketpilot.io/api/v1/buckets/12/objects/transfer \
  -H "Authorization: Bearer $BUCKETPILOT_KEY" -H "Content-Type: application/json" \
  -d '{ "wholeBucket": true, "copy": true, "destinationBucketId": 15, "toPrefix": "archive/" }'

Body: keys and/or prefixes (what to move), or wholeBucket: true for everything; destinationBucketId (another bucket; omit for the same one); toPrefix; copy (true keeps the source, false moves it). A whole-bucket transfer is copy or move only. The same plan gates as the app apply.

© 2026 BucketPilot

Loading BucketPilot