# Let customers use Object Storage

> The portal API for a customer's Object Storage buckets, objects, access keys and bucket settings.

Source: https://www.coritan.com/docs/organizations/storefront/portal-object-storage/

These portal routes let a customer use the [Object Storage](/docs/object-storage/) you sold them: make buckets, browse and upload objects, issue access keys for S3 clients, and set versioning, Object Lock, CORS and lifecycle rules. They are the customer Object Storage API on a different path. A route that is `/api/v1/client/object-storage/{service_id}/buckets` there is `/api/v1/orgs/acme/portal/object-storage/{service_id}/buckets` here, with the same request body and the same answer. This page lists what differs and walks through the tasks a storefront needs most.

## Before you begin

- Get a customer token as [Sign customers in to your storefront](/docs/organizations/storefront/customer-sign-in/) describes. Every call on this page sends it as `Authorization: Bearer $CUSTOMER_TOKEN`. Only the customer who bought a service can use it.
- Sell Object Storage in your catalogue, as [Set up products and pricing](/docs/organizations/products-and-pricing/) explains. Customers order it like any other product; see [Take an order](/docs/organizations/storefront/storefront-api/#take-an-order).

## How the portal differs from the customer API

- The paths start with `/api/v1/orgs/{org_slug}/portal/object-storage`. `service_id` is the `service_id` from `GET /portal/object-storage/services`. The same service is `org_service_id` there, which the billing routes take, such as [Change a plan](/docs/organizations/storefront/customer-portal/#change-a-plan) and [Cancel a service](/docs/organizations/storefront/customer-portal/#cancel-a-service).
- A service, bucket or key that belongs to another customer, to another organization or to a service that has ended answers `404`: `Object storage service not found`, `Bucket not found` or `Access key not found`.
- Every change needs the service to be `active`. A suspended service answers `409` with `This service is suspended; it can be changed once it is active`. Reading, downloading with a presigned `GET` URL and revoking a key work in every status.
- `rates`, `currency` and `free` are your organization's own terms for the product ([Sell Object Storage](/docs/organizations/products-and-pricing/#sell-object-storage)): the price of storage and of each million operations in `currency`, and the amounts each customer gets free every month. They are never our prices, which are what we charge your organization. `rates` and `free` are `null` only when the terms cannot be read, and `overage_per_gb_month` is always `null`. `next_due_date`, `billing_cycle` and `product_name` are the ones on your customer's service.
- Every customer has a bucket prefix of their own, such as `c42-`. The service's `bucket_prefix` and `GET .../buckets` show it. No other customer of yours, and no customer of another organization, can have a bucket whose name starts with it.
- Each customer has these budgets of changes an hour, shared by all their services: 60 bucket changes (making and deleting), 60 key changes (issuing and revoking), 60 changes to CORS and lifecycle rules together, and 300 changes to versioning, Object Lock and versions. A change over the budget answers `429` with a `Retry-After` header.
- Every change is recorded in your organization's audit log against the customer, as `object_storage.bucket_created`, `object_storage.key_issued` and so on.
- The portal has no routes for public URLs, custom domains and static websites, event notifications, migrations from other providers, replication between regions, temporary credentials or default encryption. They are for platform accounts only.

## Find the customer's services

```bash
curl "https://api.coritan.com/api/v1/orgs/acme/portal/object-storage/services" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"
```

The answer is `{"items": [...], "total": n}`, newest first, and leaves out ended services. Each item has these fields:

| Field | What it holds |
| --- | --- |
| `service_id` | The ID every other route on this page takes. |
| `org_service_id` | The service's ID in your organization, for the billing routes. |
| `label`, `hostname`, `product_name` | The names your customer sees, from your service and product. |
| `status` | Where the service stands, such as `active` or `suspended`. |
| `region`, `location_id` | The service's home region. |
| `namespace`, `bucket_prefix` | The customer's namespace, such as `c42`, and what their bucket names start with, `c42-`. |
| `bucket_count`, `key_count`, `used_bytes`, `object_count` | What the service holds now, as we last measured it. |
| `billing_model` | `payg`: billed by use. `quota_bytes` is `null`. |
| `rates`, `currency`, `free` | What your organization charges, in `currency`, and what each customer gets free each month. |
| `billing_cycle`, `next_due_date` | How and when your organization bills the service. |

`GET /portal/object-storage/{service_id}` adds `endpoints` (the S3 endpoint of each region the service has a bucket in), `max_buckets`, `presign_max_seconds` and `grace_days`. `GET /portal/object-storage/regions` lists the regions a bucket can go in, each with its `id`, `code`, `name` and `endpoint`.

## Order Object Storage for a customer

A customer orders Object Storage like any other product. List the catalogue, take the product whose `module_name` is `object_storage`, and place the order with the region in `config`:

```bash
curl "https://api.coritan.com/api/v1/orgs/acme/storefront/products"
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/storefront/order" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"org_product_id": 412, "org_pricing_id": 977, "hostname": "object-storage", "config": {"location_id": 3, "bucket_name": "assets"}}'
```

The product's `metadata.object_storage` holds your terms: `currency`, the `rates` for storage and each million operations, and the `free` amounts each customer gets every month. `config.location_id` is the `id` of a region from `GET /portal/object-storage/regions`. `config.bucket_name` is optional, and we add the customer's prefix to it.

A plan that costs nothing a month makes a $0 order. It needs no payment, and the answer has `requires_payment` set to `false`. A free order takes the checks every free order takes: `turnstile_token` when Turnstile is on, and the limit on free services for each customer, which answers `409` with `free_limit_reached` ([Take an order](/docs/organizations/storefront/storefront-api/#take-an-order)).

The `service_id` in the answer is the service's ID in your organization, which is the portal's `org_service_id`. The portal's own routes take a different ID. Ask `GET /portal/object-storage/services` until an item has that `org_service_id`, and use the `service_id` of that item. The service shows in that list with a `status` other than `active` until we have set it up. Make changes once it reads `active`.

## Make a bucket

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/object-storage/4812/buckets" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "photos", "location_id": 3, "versioning": true}'
```

The answer is `201` with the bucket. `name` is what the customer types, 1–63 characters. We add the customer's prefix when it is missing, so this bucket is `c42-photos`. A customer who types another customer's prefix, or a platform account's, gets it inside their own: `c2-photos` becomes `c42-c2-photos`. The whole name is 3–63 characters in lowercase letters, digits, hyphens and dots. It starts and ends with a letter or a digit, and has no dot next to a dot or a hyphen. Leave out `location_id` to use the service's home region.

`versioning` keeps every version of every object from the start. `object_lock` creates the bucket with Object Lock, which a customer cannot add later: `{"object_lock": {"mode": "GOVERNANCE", "days": 30}}`, with `days` or `years`. [Create and delete buckets](/docs/object-storage/buckets/) and [Protect objects with Object Lock](/docs/object-storage/object-lock/) explain both.

A name that is taken answers `409`, as does a service that already holds `max_buckets` buckets. A name that breaks a rule answers `422` with the reason.

`DELETE .../buckets/{bucket_id}` deletes a bucket. It answers `409` while the bucket holds objects, unless the request adds `?force=true`, which deletes every object and every version in it for good. A bucket with Object Lock refuses while a version is still locked. After a delete, the name stays taken for `grace_days`.

## Browse and upload objects

Every path below starts with `/api/v1/orgs/acme/portal/object-storage/{service_id}/buckets/{bucket_id}`:

| Route | What it does |
| --- | --- |
| `GET /objects` | Lists one page under `prefix`, folders first. `max_keys` is 1–1000 and `flat=true` skips folders. Pass `next_token` back as `token` for the next page. |
| `POST /objects/presign` | Returns a URL that a browser or app uses for one request, as the next block shows. |
| `DELETE /objects` | Deletes up to 1000 keys, sent as `{"keys": [...]}`. A key that fails is listed in `errors`. |

Bytes go straight between the customer's browser and storage, so your storefront never carries them. Ask for a URL, then use it:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/object-storage/4812/buckets/9120/objects/presign" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "photos/cat.jpg", "op": "put", "content_type": "image/jpeg", "expires": 600}'
```

`op` is `get`, `put` or `delete`. The answer holds `url`, `method`, `headers` and `expires_at`. Send the request with that method and every header listed, such as `Content-Type`. `expires` is 60–86400 seconds, and we cap it at the service's `presign_max_seconds`. For `get`, `download=true` makes the browser save the file, and `version_id` downloads one version. Anyone who holds the URL can use it until it expires, and we count each request made with it for the service's use. A `put` or `delete` URL needs an active service.

A browser on your storefront can use these URLs while the bucket has no CORS rules. When the customer saves CORS rules, we keep one more rule first in the bucket, `coritan-dashboard`, that allows your storefront's addresses (the ones we serve for you and your custom domain) to use `GET`, `PUT`, `HEAD` and `DELETE`. That rule never shows in the customer's own rule list.

## Issue access keys

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/object-storage/4812/keys" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label": "deploy", "mode": "read_write"}'
```

The answer is `201` with the key, its `secret_key` (shown this once), and the `endpoint` and `region` an S3 client connects to. `mode` is `read` (read and list) or `read_write`. `label` is 1–64 characters of letters, digits, spaces, dots, hyphens and underscores, and is unique in the service. Add `bucket_id` to open one bucket. Without it the key opens every bucket of the customer's service, the ones made later too. A key never makes or deletes buckets. A new key works within a couple of minutes.

`GET .../keys` lists the service's keys without their secrets, and `DELETE .../keys/{key_id}` revokes one for good. A label that is already in use answers `409`. [Create and revoke access keys](/docs/object-storage/access-keys/) and [Connect an S3 client](/docs/object-storage/connect-an-s3-client/) explain what a customer does with a key.

## Set versioning, Object Lock, CORS and lifecycle rules

Every path below starts with `/api/v1/orgs/acme/portal/object-storage/{service_id}/buckets/{bucket_id}`:

| Route | What it does |
| --- | --- |
| `PUT /versioning` | Sends `{"status": "enabled"}` or `{"status": "suspended"}`. Versioning cannot go back to off, and a bucket with Object Lock cannot be suspended. |
| `GET /object-lock`, `PUT /object-lock` | Reads or changes the default retention of a bucket made with Object Lock: `mode`, with `days` or `years`. |
| `GET /objects/versions`, `GET /objects/details` | Lists every version and delete marker, and reads one version's details. |
| `POST /objects/restore`, `DELETE /objects/version` | Makes an older version current again, or deletes one version for good. |
| `PUT /objects/legal-hold`, `PUT /objects/retention` | Puts a legal hold or a retention on one version. |
| `GET /cors`, `PUT /cors`, `DELETE /cors` | Reads, replaces or removes the bucket's CORS rules. At most 99. |
| `GET /lifecycle`, `PUT /lifecycle`, `DELETE /lifecycle` | Reads, replaces or removes the bucket's lifecycle rules. At most 100. |
| `GET /lifecycle/runs` | Lists the latest daily runs of the rules: what each removed, and what went wrong. |

The bodies and the refusals are the customer API's, which these pages describe: [Keep earlier versions of objects](/docs/object-storage/versioning/), [Protect objects with Object Lock](/docs/object-storage/object-lock/), [Set CORS rules](/docs/object-storage/cors/) and [Expire objects with lifecycle rules](/docs/object-storage/lifecycle-rules/). A rule that breaks a limit answers `422` and names the rule.

## Show use and requests

`GET .../{service_id}/usage?days=30` returns what the buckets held over time, one point for each measurement we take every hour. `GET .../{service_id}/requests?days=30` returns the requests per day, 1–90 days ending today: Class A, Class B and free operations, with the bytes uploaded and downloaded. [Usage and billing](/docs/object-storage/usage-and-billing/) explains each count. The routes show use and never prices.

## Troubleshooting

| Answer | Cause |
| --- | --- |
| `404` `Object storage service not found` | The service belongs to another customer or another organization, has ended, or is not Object Storage. |
| `409` `This service is suspended; it can be changed once it is active` | The service is not `active`. Reading still works. |
| `409` `A bucket named c42-photos already exists` | The customer already has a bucket with that name. A name deleted less than `grace_days` ago answers `409` too, with `was deleted recently and is still being removed`. |
| `422` with a reason | A name, label, rule or retention broke a rule. The reason says which. |
| `429` | The customer made more changes than the hourly budget allows. Wait `Retry-After` seconds. |

## What the MCServerHost and Minehost storefronts show

MCServerHost and Minehost both have an Object Storage area built on these routes. A customer sees the product and your rates at `/object-storage`, orders it, and then finds a list of services, an overview with the endpoint and bucket prefix, a bucket list, a file browser with upload and download, access keys with the secret shown once, and a usage page. Versioning, Object Lock, CORS rules and lifecycle rules appear as labels on a bucket. These storefronts do not change them yet, though the routes above do.

## Related

- [Object Storage](/docs/object-storage/) describes what a customer does with the service in the dashboard.
- [Build the customer account area](/docs/organizations/storefront/customer-portal/) covers the service list, plan changes and cancelling.

## API

- `GET /api/v1/orgs/{org_slug}/portal/object-storage/regions`: The regions where a bucket can be made, with each region's S3 endpoint (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage/#op-get-api-v1-orgs-org-slug-portal-object-storage-regions)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/services`: List services (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage/#op-get-api-v1-orgs-org-slug-portal-object-storage-services)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}`: One service with its endpoints and its limits (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets`: List buckets (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets)
- `POST /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets`: Make a bucket in the chosen region, or the service's home region (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-post-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}`: One bucket with its size, object count, rule counts, versioning and Object Lock (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}`: Delete a bucket (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/cors`: Get bucket cors (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-cors)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/cors`: Replace the bucket's CORS rules with the ones sent, at most 99 (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-cors)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/cors`: Remove every CORS rule from the bucket (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-c)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/lifecycle`: The bucket's lifecycle rules: what we delete from it, and when (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-life)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/lifecycle`: Replace the bucket's lifecycle rules with the ones sent, at most 100 (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-life)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/lifecycle`: Remove every lifecycle rule from the bucket, the default one included (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-l)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/lifecycle/runs`: List lifecycle runs (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-life)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/object-lock`: Whether the bucket has Object Lock, and the default retention it gives every new version (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/object-lock`: Change the default retention of a bucket that has Object Lock (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects`: One page of the bucket under prefix, folders first (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects`: Delete up to 1000 named objects (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-o)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/details`: Get object details (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/legal-hold`: Put a legal hold on a version, or take it off (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `POST /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/presign`: A short-lived URL a browser or app uses directly for one GET, PUT or DELETE of one object (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-post-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obj)
- `POST /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/restore`: Restore object version (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-post-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obj)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/retention`: Give a version a retention, or change the one it has (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/version`: Delete one version or delete marker for good (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-o)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/objects/versions`: List object versions (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-obje)
- `PUT /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/buckets/{bucket_id}/versioning`: Turn versioning on for a bucket, or suspend it (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-buckets/#op-put-api-v1-orgs-org-slug-portal-object-storage-service-id-buckets-bucket-id-vers)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/keys`: The service's access keys (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-keys/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-keys)
- `POST /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/keys`: Issue an access key for S3 clients (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-keys/#op-post-api-v1-orgs-org-slug-portal-object-storage-service-id-keys)
- `DELETE /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/keys/{key_id}`: Revoke an access key (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage-keys/#op-delete-api-v1-orgs-org-slug-portal-object-storage-service-id-keys-key-id)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/requests`: Requests to the service per day, for the last days days (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-requests)
- `GET /api/v1/orgs/{org_slug}/portal/object-storage/{service_id}/usage`: Stored bytes and objects over time, as we measured them each hour (https://www.coritan.com/docs/api/reference/organizations/customer-portal/object-storage/#op-get-api-v1-orgs-org-slug-portal-object-storage-service-id-usage)
