Object Storage API

Manage S3-compatible object storage buckets and access keys programmatically through the DanubeData API.

Endpoints Overview

Storage Buckets

MethodEndpointDescriptionScope Required
GET/api/v1/storage/bucketsList all bucketsstorage:read
POST/api/v1/storage/bucketsCreate a new bucketstorage:write
GET/api/v1/storage/buckets/{id}Get bucket detailsstorage:read
PUT/api/v1/storage/buckets/{id}Update bucket settingsstorage:write
DELETE/api/v1/storage/buckets/{id}Delete a bucketstorage:delete
GET/api/v1/storage/buckets/{id}/metricsGet bucket metricsstorage:read
GET/api/v1/storage/buckets/{id}/metrics/trendHistorical time-series metricsstorage:read
GET/api/v1/storage/buckets/{id}/metrics/top-objectsTop objects by size / egress / requestsstorage:read
GET/api/v1/storage/buckets/{id}/metrics/healthBucket health (multipart, deleted versions)storage:read

Storage Access Keys

MethodEndpointDescriptionScope Required
GET/api/v1/storage/access-keysList all access keysstorage:read
POST/api/v1/storage/access-keysCreate a new access keystorage:write
GET/api/v1/storage/access-keys/{id}Get access key detailsstorage:read
DELETE/api/v1/storage/access-keys/{id}Revoke an access keystorage:delete

Bucket Policy

MethodEndpointDescriptionScope Required
GET/api/v1/storage/buckets/{id}/policyGet a bucket's custom policy statements and its effective policystorage:read
PUT/api/v1/storage/buckets/{id}/policyReplace the custom policy statementsstorage:read and storage:write
POST/api/v1/storage/buckets/{id}/policy/folder-grantsGive an access key access to a folderstorage:read and storage:write

Storage Buckets

List All Buckets

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "data": [
    {
      "id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
      "name": "my-bucket",
      "display_name": "My Application Bucket",
      "status": "active",
      "status_label": "Active",
      "region": "fsn1",
      "endpoint_url": "https://s3.danubedata.ro",
      "public_access": false,
      "versioning_enabled": false,
      "encryption_enabled": true,
      "size_bytes": 1073741824,
      "size_human": "1.00 GB",
      "object_count": 150,
      "monthly_cost_cents": 500,
      "monthly_cost_dollars": 5.00,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}

Create a Bucket

Bash
curl -X POST 'https://danubedata.ro/api/v1/storage/buckets' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "name": "my-new-bucket",
    "display_name": "My New Bucket",
    "region": "fsn1",
    "versioning_enabled": false,
    "public_access": false,
    "encryption_enabled": true,
    "tags": {
      "environment": "production",
      "project": "webapp"
    }
  }'

Parameters:

ParameterTypeRequiredDescription
namestringYesBucket name (3-63 chars, lowercase, alphanumeric and hyphens)
display_namestringNoHuman-readable display name
regionstringYesRegion code (e.g., fsn1)
versioning_enabledbooleanNoEnable object versioning (default: false)
public_accessbooleanNoAllow public read access (default: false)
encryption_enabledbooleanNoEnable server-side encryption (default: true)
encryption_typestringNoEncryption type: sse-s3 or sse-kms
cors_configurationarrayNoCORS rules for the bucket
lifecycle_rulesarrayNoObject lifecycle rules
tagsobjectNoKey-value tags for the bucket

Response (201 Created):

JSON
{
  "message": "Storage bucket created successfully",
  "bucket": {
    "id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
    "name": "my-new-bucket",
    "status": "pending",
    "status_label": "Pending"
  }
}

Get Bucket Details

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "bucket": {
    "id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
    "name": "my-bucket",
    "display_name": "My Application Bucket",
    "status": "active",
    "region": "fsn1",
    "endpoint_url": "https://s3.danubedata.ro",
    "public_url": null,
    "public_access": false,
    "versioning_enabled": false,
    "encryption_enabled": true,
    "encryption_type": "sse-s3",
    "size_bytes": 1073741824,
    "object_count": 150,
    "custom_policy_statements": null,
    "bucket_arn": "arn:aws:s3:::dd-42-my-bucket",
    "tags": {
      "environment": "production"
    },
    "monthly_cost_cents": 500,
    "monthly_cost_dollars": 5.00,
    "can_be_modified": true,
    "can_be_destroyed": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  },
  "endpoint": "https://s3.danubedata.ro"
}

Update Bucket Settings

Bash
curl -X PUT 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "display_name": "Updated Bucket Name",
    "versioning_enabled": true,
    "public_access": false,
    "tags": {
      "environment": "staging"
    }
  }'

Response:

JSON
{
  "message": "Bucket settings updated successfully",
  "bucket": {
    "id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
    "name": "my-bucket",
    "display_name": "Updated Bucket Name",
    "versioning_enabled": true
  }
}

Delete a Bucket

Bash
curl -X DELETE 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "message": "Bucket deletion initiated",
  "status": "destroying"
}

Note: Bucket deletion is asynchronous. The bucket and all its objects will be permanently deleted.

Get Bucket Metrics

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "size_bytes": 1073741824,
  "size_human": "1.00 GB",
  "object_count": 150,
  "requests_24h": 48213,
  "requests_24h_by_method": { "GET": 41002, "PUT": 6100, "DELETE": 1111 },
  "requests_24h_by_status": { "2xx": 47800, "4xx": 390, "5xx": 23 },
  "error_rate_24h": 0.0086,
  "latency_24h_ms": { "p50": 12.4, "p95": 58.1, "mean": 18.7 },
  "egress_bytes_24h": 5368709120,
  "egress_human_24h": "5.00 GB",
  "ingress_bytes_24h": 1073741824,
  "ingress_human_24h": "1.00 GB",
  "monthly_cost_cents": 500,
  "monthly_cost_dollars": 5.00,
  "source": "deltas",
  "freshness": "fresh",
  "last_sync_at": "2024-01-15T12:00:00Z",
  "metrics_precomputed_at": "2024-01-15T12:00:00Z"
}

Notes:

  • freshness is fresh, lagging, or stale — how recent the metrics are.
  • source is deltas when the per-interval metrics pipeline has data for the bucket, or legacy for older buckets where the status/latency/ingress fields are null. Skip absent dimensions when source is legacy.

Get Metrics Trend

Historical time-series for a bucket.

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/trend?window=24h' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Query parameters:

ParameterValuesDefaultDescription
window1h, 6h, 12h, 24h, 3d, 7d, 14d, 30d24hTime range
resolution1m, 5m, 10m, 15m, 30m, 1h, 6h, 1dauto (from window)Bucket interval for each data point

Response:

JSON
{
  "bucket_id": "01HXYZ...",
  "window": "24h",
  "resolution": "5m",
  "source": "deltas",
  "freshness": "fresh",
  "generated_at": "2024-01-15T12:00:00Z",
  "data": [ ... ]
}

Get Top Objects

Top-N objects in a bucket by size, egress, or request count. Powered by an hourly probe — returns an empty items array until the first snapshot has run.

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/top-objects?dimension=size&limit=10' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Query parameters:

ParameterValuesDefaultDescription
dimensionsize, egress, requestssizeRanking dimension
limit1–5010Number of objects to return

Response:

JSON
{
  "bucket_id": "01HXYZ...",
  "dimension": "size",
  "recorded_at": "2024-01-15T11:00:00Z",
  "items": [
    { "rank": 1, "object_key": "backups/2024-01-15.tar.gz", "value": 2147483648 },
    { "rank": 2, "object_key": "media/video.mp4", "value": 734003200 }
  ]
}

Get Bucket Health

Reclaimable-space and freshness indicators. Values are null until the hourly health probe has visited the bucket.

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/health' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "bucket_id": "01HXYZ...",
  "pending_multipart_count": 3,
  "pending_multipart_bytes": 15728640,
  "deleted_size_bytes": 524288000,
  "freshness": "fresh",
  "metrics_precomputed_at": "2024-01-15T12:00:00Z",
  "last_health_check_at": "2024-01-15T11:00:00Z"
}

Storage Access Keys

Access keys provide S3-compatible credentials for accessing your storage buckets programmatically.

List All Access Keys

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/access-keys' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "data": [
    {
      "id": "abc12345-6789-0abc-def1-234567890abc",
      "name": "Production API Key",
      "access_key_id": "DDAK1234567890EXAMPLE",
      "access_key_id_masked": "DDAK****XAMPLE",
      "arn": null,
      "status": "active",
      "status_label": "Active",
      "permissions": null,
      "expires_at": null,
      "last_used_at": "2024-01-15T11:30:00Z",
      "is_expired": false,
      "created_at": "2024-01-10T09:00:00Z",
      "updated_at": "2024-01-15T11:30:00Z"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}

Create an Access Key

Bash
curl -X POST 'https://danubedata.ro/api/v1/storage/access-keys' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "name": "My Application Key",
    "expires_at": "2025-01-15T00:00:00Z"
  }'

Parameters:

ParameterTypeRequiredDescription
namestringYesA descriptive name for the access key
expires_atdatetimeNoExpiration date (ISO 8601 format)
scopestringNoteam (the default) for a key that reaches every bucket, buckets for a key restricted to the buckets in bucket_permissions, or none for a key with no access of its own: it reaches only what a bucket policy allows it
bucket_permissionsarrayConditionalRequired if scope is buckets: 1 to 50 entries, each with a bucket_id (the id of one of your buckets, in the status active or updating; a bucket in any other status is refused with 422) and a level (read, readwrite or full). For team and none, leave it out or send an empty list. Send it only together with a scope: without one it is refused, not taken for a team-wide key

Response (201 Created):

JSON
{
  "id": "abc12345-6789-0abc-def1-234567890abc",
  "name": "My Application Key",
  "access_key_id": "DDAK1234567890EXAMPLE",
  "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "arn": null,
  "expires_at": "2025-01-15T00:00:00Z",
  "message": "Access key created. Make sure to save the secret key - it will not be shown again."
}

Important: The secret_access_key is only returned once during creation. Store it securely as it cannot be retrieved later.

arn: A key created with scope set to buckets or none gets a storage user of its own, and arn is that user's ARN, for example arn:aws:iam:::user/team-42-sk-k3j9x2mq. Name it as the principal of a statement in a bucket policy when the statement should apply to that key alone. arn is null for a key that signs as your team, an identity all such keys share. The list and detail responses carry the same field. See S3 Access Keys.

Get Access Key Details

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/access-keys/{access_key_id}' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "access_key": {
    "id": "abc12345-6789-0abc-def1-234567890abc",
    "name": "My Application Key",
    "access_key_id": "DDAK1234567890EXAMPLE",
    "access_key_id_masked": "DDAK****XAMPLE",
    "arn": null,
    "status": "active",
    "status_label": "Active",
    "expires_at": "2025-01-15T00:00:00Z",
    "last_used_at": "2024-01-15T11:30:00Z",
    "is_expired": false,
    "created_at": "2024-01-10T09:00:00Z"
  }
}

Note: The secret_access_key is never returned after initial creation.

Revoke an Access Key

Bash
curl -X DELETE 'https://danubedata.ro/api/v1/storage/access-keys/{access_key_id}' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "message": "Access key has been revoked"
}

Warning: Revoking an access key immediately invalidates it. Any applications using this key will lose access.

Bucket Policy

A bucket's policy decides who can reach the bucket and what they may do there. It has two parts: the custom statements you write, which these endpoints read and replace, and the statements the platform manages for you (the public access setting, the buckets chosen for scoped access keys, and its own access), which you never restate. The effective policy is the whole document, both parts merged, that the storage gateway enforces. See Bucket Policies for what a statement can say.

The policy endpoints work on the buckets that have the policy editor in the console, which are the buckets on the current high-durability storage endpoint. For any other bucket they answer 404 Not Found, and so they do for a bucket that belongs to another project.

A change is saved at once and applied to the storage gateway in the background. PUT and POST answer 202 Accepted with status set to updating, which means the change is on its way. active means none is being applied any more, and a change the gateway refused ends there too, so active alone does not prove that the gateway holds the change. To be sure, read the bucket's policy with S3 GetBucketPolicy, or send the same request again, which sends the document to the gateway again.

A change that arrives while the bucket's policy is being applied waits for up to five seconds. If the bucket is still busy by then, nothing is saved and the answer is 409 Conflict with Retry-After: 5: send the request again.

JSON
{"error": "The bucket's policy is being applied. Try again in a moment."}

GET needs the storage:read ability. PUT and POST need both storage:read and storage:write, because their answer is the policy.

When a change alters the stored statements, the person whose API token made it gets a "bucket policy changed" security email that links to the bucket's Access tab. The console asks you to confirm it's you before it saves a policy. A token cannot answer that, so the email is the safeguard over the API, as it is when a token creates an access key. Repeating a PUT or a folder grant that changes nothing sends no email, but it still sets the bucket to updating and sends the document to the gateway again, which is also how you retry a change that did not reach it.

Get a Bucket's Policy

Bash
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'

Response:

JSON
{
  "custom_policy_statements": [],
  "effective_policy": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "PlatformOwnerObjectTagging",
        "Effect": "Allow",
        "Principal": {"AWS": ["arn:aws:iam:::user/team-42"]},
        "Action": [
          "s3:GetObjectTagging",
          "s3:GetObjectVersionTagging",
          "s3:PutObjectTagging",
          "s3:DeleteObjectTagging"
        ],
        "Resource": ["arn:aws:s3:::dd-42-projects/*"]
      }
    ]
  },
  "status": "active"
}
FieldDescription
custom_policy_statementsThe custom statements stored for the bucket, as they were saved. An empty list when there are none
effective_policyThe policy document the gateway enforces. Each custom statement carries a Sid of the form Custom1, Custom2 and so on, in the order stored. DanubeData adds its own management addresses to an IP allow-list (a Deny with NotIpAddress) at the gateway, and they are not listed here: the list in such a statement is the one you saved
statusupdating means a change is on its way to the gateway. active means none is being applied any more, and a change the gateway refused ends there too

Get Bucket Details also returns the stored statements as custom_policy_statements, which is null when there are none, and the bucket's ARN as bucket_arn.

Replace the Custom Statements

Bash
curl -X PUT 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "custom_policy_statements": [
      {
        "Effect": "Deny",
        "Principal": "*",
        "Action": ["s3:GetObject"],
        "Resource": ["arn:aws:s3:::dd-42-projects/*"],
        "Condition": {"NotIpAddress": {"aws:SourceIp": ["203.0.113.0/24"]}}
      }
    ]
  }'

Response (202 Accepted): the same document as Get a Bucket's Policy, with the new statements in custom_policy_statements and effective_policy and status set to updating.

Parameters:

ParameterTypeRequiredDescription
custom_policy_statementsarrayYesThe statements to store, as a list even when there is only one. They replace every custom statement the bucket has now, and an empty list removes them all

A statement is checked exactly as the console checks it when you save a policy there:

  • Effect is Allow or Deny. A Deny cannot use wildcards in its actions, and cannot name the actions that manage the policy itself, so a policy can never lock you or DanubeData out of the bucket.
  • Principal is "*" or {"AWS": [...]} with the ARNs of your own project's access keys, which the access key endpoints return as arn. A key of another project, an account id or a role is refused.
  • Resource is optional and has to be the bucket (arn:aws:s3:::dd-42-projects) or objects in it. The bucket's ARN is bucket_arn in the bucket details.
  • Condition is optional: IpAddress or NotIpAddress on aws:SourceIp, or StringLike or StringEquals on s3:prefix. The s3:prefix conditions are accepted only in an Allow statement whose actions are s3:ListBucket, s3:ListBucketVersions or both.
  • A bucket takes at most 100 custom statements and 20 KB in all.
  • A statement whose Sid is one the platform sets itself, such as PublicReadGetObject or ScopedKeysFull, is dropped.

A statement that is refused comes back as a 422 that names it by its position in the list you sent, and nothing is saved:

JSON
{
  "message": "Validation failed.",
  "errors": {
    "custom_policy_statements.0.Principal": [
      "Principal \"arn:aws:iam:::user/team-7-sk-zz99zz99\" is not an access key of this team. Use \"*\" for everyone, or the ARN of one of this team's access keys (arn:aws:iam:::user/<access-key-uid>)."
    ]
  }
}

Give a Key Access to a Folder

A folder grant is two statements: one that lets a key work on the objects under a folder, and one that lets it list that folder. This endpoint writes them for you, adds the ones that are not stored yet after the custom statements that are stored, and saves the result, so a script does not have to read the policy, edit it and send it back.

This example gives one key read and write access to the invoices/ folder of a bucket. Create the key with scope set to none first: it then has an identity of its own and no access until a policy grants some, so the folder is all it reaches.

Bash
curl -X POST 'https://danubedata.ro/api/v1/storage/access-keys' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"name": "invoices-service", "scope": "none"}'

The response carries the key's id, which is what a grant names, and its arn. Then give it the folder:

Bash
curl -X POST 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy/folder-grants' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "key_id": "abc12345-6789-0abc-def1-234567890abc",
    "folder": "invoices",
    "level": "readwrite"
  }'

Parameters:

ParameterTypeRequiredDescription
key_idstringYesThe id of the access key, as the access key endpoints return it. It is not the key's S3 access_key_id
folderstringOne of folder and whole_bucketThe folder to give access to, such as invoices or reports/2026. A leading or trailing slash makes no difference. It cannot contain * or ?, the two characters ${ (which start a policy variable; a lone $ is fine), control characters or invisible formatting characters, and may be at most 1000 bytes long
whole_bucketbooleanOne of folder and whole_buckettrue, with no folder, to give access to the whole bucket. A missing or empty folder is never taken for the whole bucket
levelstringYesread to get objects, readwrite to also add and overwrite them, or full to also delete them

Response (202 Accepted), for a bucket that had no custom statements before:

JSON
{
  "added_statements": [
    {"Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"], "Resource": ["arn:aws:s3:::dd-42-projects/invoices/*"]},
    {"Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:ListBucket", "s3:ListBucketVersions"], "Resource": ["arn:aws:s3:::dd-42-projects"], "Condition": {"StringLike": {"s3:prefix": ["invoices/*"]}}}
  ],
  "warnings": [],
  "custom_policy_statements": [
    {"Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"], "Resource": ["arn:aws:s3:::dd-42-projects/invoices/*"]},
    {"Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:ListBucket", "s3:ListBucketVersions"], "Resource": ["arn:aws:s3:::dd-42-projects"], "Condition": {"StringLike": {"s3:prefix": ["invoices/*"]}}}
  ],
  "effective_policy": {
    "Version": "2012-10-17",
    "Statement": [
      {"Sid": "PlatformOwnerObjectTagging", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42"]}, "Action": ["s3:GetObjectTagging", "s3:GetObjectVersionTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging"], "Resource": ["arn:aws:s3:::dd-42-projects/*"]},
      {"Sid": "Custom1", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"], "Resource": ["arn:aws:s3:::dd-42-projects/invoices/*"]},
      {"Sid": "Custom2", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]}, "Action": ["s3:ListBucket", "s3:ListBucketVersions"], "Resource": ["arn:aws:s3:::dd-42-projects"], "Condition": {"StringLike": {"s3:prefix": ["invoices/*"]}}}
    ]
  },
  "status": "updating"
}

added_statements are the statements this request added, and warnings says what the grant does not do (below). The other fields are the policy as it stands, as in Get a Bucket's Policy. When the change has reached the gateway, the key can read, write and list under invoices/, and aws s3 ls s3://dd-42-projects/invoices/ with its credentials works. A status of active does not prove that it has: see above for how to be sure.

How a grant behaves:

  • It adds, it does not replace. The statements that are not stored yet go after the statements that are stored, and the whole list that results is checked as a replace is, including the limits of 100 statements and 20 KB. When a statement that is already stored no longer passes (the error names it by its position in custom_policy_statements), fix it with a replace first.
  • Asking twice changes nothing. A statement that is stored already, with the same contents, is not added again, so repeating a request returns added_statements empty and sends no email. Another spelling of the same folder, such as invoices/ or /invoices, is the same grant.
  • Grants add up. Giving the same key and folder a higher level adds only the object statement for it, and the narrower one stays. To narrow access, replace the custom statements.
  • Which keys. The key has to belong to the project, be active and not expired, and have an identity of its own: its arn is not null. A key that signs as your team already has full access to every bucket, so a grant for it is refused. The answer is the same for a key that does not exist as for one that belongs to another project.
  • Keys that have access already. A key created for chosen buckets (scope set to buckets) keeps its access to the whole of those buckets, and a folder grant adds to it: a grant never narrows a key. When the key you name reaches this bucket that way, warnings says so: "This key already reaches the whole bucket through its bucket permissions (read). The folder grant adds to that; it does not limit the key to the folder. To limit it to the folder, revoke this key and create a key with scope set to none." A key created with scope set to none has no access of its own, and reaches only what its grants give it.

Errors: a refused grant is a 422 that names the field, and nothing is saved:

JSON
{
  "message": "Validation failed.",
  "errors": {
    "key_id": ["Choose an active access key of this team."]
  }
}

Limits of Bucket Policies

  • A bucket takes about 30 full-access folder grants. Custom statements are limited to 20 KB and a full-access grant takes roughly 500 to 650 bytes, so there are fewer with long bucket and folder names, and more with read-only grants.
  • A key that is limited to a folder cannot list the root of the bucket. Point an S3 browser or client at the folder, as in s3://dd-42-projects/invoices/.
  • A folder grant gives the object actions of its level and listing, and nothing else. It does not include s3:GetBucketLocation, the other reads of the bucket's configuration, or s3:ListBucketMultipartUploads. A client that needs them takes statements of its own, which you can add by replacing the custom statements.
  • Revoking an access key leaves its statements in the policy. They grant nothing, and you can remove them by replacing the custom statements.

Using Access Keys with S3 Clients

Once you have created an access key, you can use it with any S3-compatible client.

AWS CLI

Bash
aws configure --profile danubedata

# Enter your credentials when prompted:
# AWS Access Key ID: DDAK1234567890EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: fsn1

# Use with custom endpoint
aws s3 ls s3://my-bucket \
  --endpoint-url https://s3.danubedata.ro \
  --profile danubedata

Python (boto3)

Python
import boto3

s3 = boto3.client(
    's3',
    endpoint_url='https://s3.danubedata.ro',
    aws_access_key_id='DDAK1234567890EXAMPLE',
    aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
    region_name='fsn1'
)

# List objects in a bucket
response = s3.list_objects_v2(Bucket='my-bucket')
for obj in response.get('Contents', []):
    print(obj['Key'])

JavaScript (AWS SDK v3)

JavaScript
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';

const s3 = new S3Client({
  endpoint: 'https://s3.danubedata.ro',
  region: 'fsn1',
  credentials: {
    accessKeyId: 'DDAK1234567890EXAMPLE',
    secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
  },
  forcePathStyle: true
});

const command = new ListObjectsV2Command({ Bucket: 'my-bucket' });
const response = await s3.send(command);
console.log(response.Contents);

Error Responses

422 Unprocessable Entity

JSON
{
  "error": "You have reached the maximum number of buckets (10). Please delete a bucket to create a new one."
}
JSON
{
  "error": "Bucket cannot be modified in its current state",
  "status": "creating"
}

404 Not Found

JSON
{
  "error": "Bucket not found"
}

403 Forbidden

Returned when your API token lacks the required storage:* scope.

JSON
{
  "message": "Insufficient permissions"
}

Rate Limits

Storage API endpoints have specific rate limits:

EndpointLimit
Create bucket40/min, 200/hour, 2000/day
Create access key20/min, 100/hour, 1000/day
Get metrics2x standard limits
Other endpointsStandard tier limits

See Rate Limits for more details.