Skip to main content
Object storage gives you buckets of keys and bytes, reachable two ways. You manage a bucket — create it, set its policy, lifecycle, versioning and encryption — on the regional storage API. You read and write objects with any AWS S3 client, pointed at a separate S3-compatible endpoint.

Bucket management

https://storage.<region>.basaltic.shSigned with the Basaltic scheme, like every other Basaltic API. Buckets and their sub-resources live under /v1/buckets.

S3 wire protocol

https://objects.<region>.basaltic.cloudSigned as S3 (AWS SigV4). This is what you give to boto3, the AWS CLI, or anything else that speaks S3.
The S3 endpoint is deliberately on a different domain — basaltic.cloud, not the product domain. Buckets serve content that gets embedded in other people’s sites, so that traffic is kept off the domain your console session and API credentials belong to. Nothing you serve from a bucket can ride a product-domain cookie.
Both surfaces are the same storage. A bucket created through the storage API is immediately visible to an S3 client, and an object written by boto3 is immediately readable through GET /v1/buckets/{bucket}/objects/{key}.

Creating a bucket

Bucket names follow the S3 rules, and they are checked in full:
3–63 characters
required
Lowercase letters, digits and hyphens. Must start and end with a letter or digit, must not contain a double hyphen (--), and must not be shaped like an IP address.
A bucket name is unique across the whole region, not just your account. A name another account already holds comes back 409. Creating a bucket you already own is a no-op that succeeds, so a repeated create is safe.
Bucket count is capped by your organization’s buckets quota; exhausting it is also a 409 on create.

Object Lock has to be decided here

Object Lock can only be enabled at creation. There is no call that turns it on later — you would have to create a new bucket and copy the objects across. Enabling it also turns versioning on, because a lock has nothing to hold onto without versions.
PUT /v1/buckets/{bucket}/object-lock updates the default retention rule on a bucket that already has Object Lock enabled. Against a bucket that does not, it is a 409.

Pointing an S3 client at it

Set a custom endpoint and sign with your Basaltic access key. Nothing else about the client changes.
The same access keys you use everywhere else. A service account’s long-lived key needs nothing extra; temporary credentials from STS — a role session or a user session — must also carry the session token, and are rejected without it.See authentication for how to obtain each.
Both styles work. Virtual-hosted (https://my-app-assets.objects.sa-saopaulo-1.basaltic.cloud/key) is what most SDKs default to; path-style (https://objects.sa-saopaulo-1.basaltic.cloud/my-app-assets/key) is available through your client’s addressing-style option.
Set region_name to the Basaltic region code. The value is not checked against the region serving the request — it only has to match what your client signed with — so a tool hard-wired to us-east-1 still works. GetBucketLocation reports the real region.
A signed request must be within 15 minutes of the server’s clock, or it is rejected as too skewed. Presigned URLs are supported with an expiry between 1 second and 7 days, and a URL dated in the future beyond the skew tolerance is refused rather than becoming valid later.

What the S3 endpoint serves

The endpoint is verified against a real AWS SDK rather than a specification of our own — if boto3 can do it and gets S3’s error codes back, it works. What is routed today:

Buckets

ListBuckets, CreateBucket, HeadBucket, DeleteBucket, GetBucketLocation, and the ?policy, ?cors, ?lifecycle, ?versioning, ?encryption, ?tagging, ?object-lock and ?acl sub-resources.

Objects

PutObject, GetObject (including range requests), HeadObject, DeleteObject, DeleteObjects, CopyObject, ListObjects, ListObjectsV2, ListObjectVersions, and the ?tagging, ?retention, ?legal-hold and ?acl sub-resources.

Multipart

CreateMultipartUpload, UploadPart, UploadPartCopy, ListParts, ListMultipartUploads, CompleteMultipartUpload, AbortMultipartUpload.

Payload signing

Signed payloads, UNSIGNED-PAYLOAD, and signed aws-chunked streaming uploads. The body is re-hashed as it streams, so a body that does not match what was signed is rejected mid-flight.
Anything outside that list answers NotImplemented. Preflight OPTIONS requests are answered without a signature, because browsers never sign them.

Objects and their limits

These are S3’s own numbers, and they are enforced at S3’s values:
The single-upload ceiling is checked against the size you declare, before any payload is read. An oversized Content-Length is refused with EntityTooLarge (413 on the storage API) without transferring a byte — and a body that streams past the limit without declaring it is cut off too.
The 5 MiB floor applies to every part except the last one named at completion, and it is checked at completion, not when the part is uploaded. A part list whose non-final part is short fails with EntityTooSmall after the bytes are already staged.
The assembled object is written out in full inside the request that completes the upload. Budget roughly 85 MiB/s for that write, and note that the public edge ends any request after one hour. Objects beyond a few hundred GiB need a completion path this API does not offer yet.
Uploads you start and never finish keep their staged parts until something removes them. Add an abort_incomplete_multipart_upload lifecycle rule rather than relying on remembering.

Storage classes

Two classes, and they name device tiers rather than access patterns: They keep S3’s uppercase spelling because the vocabulary is S3’s. Set a class per object with x-amz-storage-class on the upload, or move objects between classes with a lifecycle transition. The two classes bill separately, so the label an object carries decides which counter and which price its bytes land on. An unsupported class — STANDARD_IA, GLACIER, anything else S3 defines — is rejected rather than quietly stored as STANDARD.

Versioning

PUT /v1/buckets/{bucket}/versioning moves a bucket between three states:
never configured
The bucket has no versioning history. On the S3 wire this is reported by omitting the status element entirely, which is how S3 itself reports it.
every write creates a version
Deletes write a delete marker instead of removing bytes. GET /v1/buckets/{bucket}/object-versions lists versions and delete markers together.
was on, now off
Existing versions are kept; new writes stop creating them. This is distinct from disabled, and the distinction matters: suspending does not delete history.
The storage API uses the platform’s lowercase vocabulary; the S3 endpoint spells the same states Enabled and Suspended in its XML. Either spelling is accepted on input.
Versions you no longer need are not free — they count against your stored bytes. Pair versioning with a noncurrent_version_expiration lifecycle rule, or a suspended bucket quietly keeps every version it ever made.

Object Lock and retention

On a bucket created with object_lock_enabled, individual objects can carry a retention mode and a retain-until date, set at upload with X-Amz-Object-Lock-Mode and X-Amz-Object-Lock-Retain-Until-Date, or afterwards through the ?retention sub-resource.

GOVERNANCE

Retention can be shortened or a locked object deleted, but only by a caller who sends X-Amz-Bypass-Governance-Retention: true and holds storage:BypassGovernanceRetention on the object.

COMPLIANCE

Nothing bypasses it. A ?retention write that would move the date earlier is refused, so retention can only ever be extended.
A legal hold (?legal-hold) is independent of the retention date: while it is on, the object cannot be deleted regardless of when retention expires. A delete blocked by either shows up as 403, not a silent no-op.

Encryption

Server-side encryption is opt-in, and it is off until you turn it on. Two ways:
The bucket default applies to writes that carry no header of their own — a per-request header wins over it. Only AES256 is supported; aws:kms and customer-provided keys are rejected with a clear error rather than being silently downgraded.
A multipart upload carries no encryption header of its own on each part: the decision is made when the upload is created and survives to the assembled object. Check ServerSideEncryption on a HEAD of the finished object, not on the create response.

Lifecycle rules

PUT /v1/buckets/{bucket}/lifecycle replaces the whole configuration. Each rule needs a status and at least one action; a rule with status: disabled stays in the configuration but is skipped during evaluation.
Rules are evaluated independently, so an object matching several is subject to all their actions. filter.prefix narrows a rule; an absent or empty filter applies it to the whole bucket.
A transition keeps the object’s identity — same key, same version id, same last-modified — and moves only its bytes. That is what makes pairing a transition with an expiration work: the move does not restart the expiry clock.
Two constraints on transitions, both rejected at write time rather than failing silently later:
  • A transition must come strictly before the rule’s expiration. Otherwise the object would be deleted before it ever moved.
  • One transition per rule. With two storage classes a second has nowhere to go. The S3 endpoint accepts a single-element <Transition> list and rejects longer ones rather than applying the first and ignoring the rest.

Quotas

Two counters, both scoped to your organization within a region: object_storage_gb is enforced by comparing what you actually store against the limit, not by a running counter. That means space freed by a lifecycle expiration, a version delete or a bucket purge comes back on its own — there is no counter left holding a phantom charge against your cap.

Access control

Two independent layers decide every object request:

IAM policies

What your own principals may do, written against crn:storage:<region>:<account>:bucket/<name> for the bucket and crn:storage:<region>:<account>:bucket/<name>/<key> for an object. See writing policies.

The bucket policy

A document attached to the bucket itself, at PUT /v1/buckets/{bucket}/policy. This is what grants access to principals outside your account — including anonymous readers.
An anonymous request is evaluated against the bucket policy alone: no policy, no access. For an authenticated caller, an explicit deny in either layer wins outright; otherwise an allow from either layer is enough to proceed.
A bucket policy that allows anonymous storage:GetObject makes those objects public to the internet. There is no second switch guarding it — the policy is the switch.
Object tags are policy context, so access can be fenced on the object itself: s3:ExistingObjectTag/<key> matches what is already on the object, s3:RequestObjectTag/<key> what a write is trying to set, and basalt:TagKeys is the set of keys a request carries. Both layers stop applying the moment the bucket owner’s organization is suspended — cross-account grantees and anonymous readers of a public bucket included.

Deleting a bucket

DELETE /v1/buckets/{bucket} does two quite different things depending on whether deletion protection is on:
The bucket must be empty. Any remaining objects, versions or in-flight multipart uploads make it a 409 BucketNotEmpty. An empty bucket is deleted immediately and the quota slot is released.
recovery_days is clamped to 1–30; 0 uses the default of 7 days.

Working with objects through the storage API

You do not need an S3 client. The storage API exposes the same object plane under /v1/buckets/{bucket}/objects, signed the Basaltic way like every other API call — useful for a service that already holds Basaltic credentials and should not carry an S3 SDK:
Per-object sub-resources ride as query parameters on these routes — ?tagging, ?retention, ?legal-hold — and the same X-Amz-* headers apply on upload for storage class, encryption and object-lock settings. delimiter gives you the usual folder semantics through common_prefixes, and max_keys is capped at 1000 per page.
Bulk delete (POST ?delete) and server-side copy (x-amz-copy-source) are S3 endpoint features. Through the storage API, delete objects one at a time.

Troubleshooting

The secret does not match the access key, or something the client signed was rewritten in transit. Check first that the credential is right, then that your client is signing with SigV4 (signature_version="s3v4") — the endpoint accepts nothing older.A key or query value containing a space or a + is a classic case: those have to be percent-encoded in the canonical request, and an SDK doing it correctly will interoperate.
Your clock is more than 15 minutes from the server’s. Fix time sync on the machine making the request; there is no way to widen the window.
An explicit deny in either layer wins, so start there: a bucket policy denying something is enough to block a caller their IAM policies allow, and the reverse holds too. With no explicit deny anywhere, you need an allow from one of the two — a bucket policy that names other principals is not itself a denial, but it will not stand in for the IAM grant you are missing.Deleting an object also fails with 403 when it is protected by an active retention period or a legal hold, which reads like a permission problem but is not one.
Objects, versions, or in-flight multipart uploads remain. List uploads with GET /v1/buckets/{bucket}/multipart-uploads — an abandoned upload holds the bucket open just as an object does, and does not show in an object listing.
A part other than the last one is under 5 MiB. The floor is checked when the upload completes, so this surfaces after every part has been staged. Re-upload with larger parts.
A bucket default applies to writes made after it was set; existing objects are untouched. Re-upload anything already stored if you need it covered. Check ServerSideEncryption on a HEAD of the stored object, which is the only answer that reflects what was written.

S3 error codes

The S3 endpoint answers in S3’s own XML vocabulary, so an SDK’s error handling works unchanged:

Next

Block storage

Volumes, snapshots and snapshot policies.

Authentication

Access keys, temporary credentials and session tokens.

Writing policies

IAM statements, bucket policies and tag conditions.

API reference

Every bucket operation, with request and response schemas.