# Create an access key

Creates an access key and hands it back together with its secret access key, ready for an S3 client
or another program. The new key always stays within the key you call with: the same permissions or
fewer, buckets among its buckets, and an object prefix inside its prefix. That makes it safe to give
a program a key that mints keys - whatever it creates can never reach further than it can itself.

An `Idempotency-Key` header makes the request safe to retry: a retry hands back the key the first
request created, and no second key is made. [Idempotency](https://docs.strg.com/rest-api/create-access-key#idempotency) has the details.

## When you'd use it

- **A CI pipeline minting a key per deploy.** The pipeline holds one key with *Manage access keys
  through the API*, creates a key limited to the release bucket for the job, and deletes it when the
  job is done.
- **Onboarding a new service.** A provisioning script gives each new service its own key, limited
  to its own bucket and prefix, so that no two services share a secret.
- **An agent or tool that sets itself up.** A program can ask for a narrower key for one task -
  read-only, say, on one bucket - instead of carrying its own broad key into that task.
- **Rotating a key.** A new key with the same permissions comes first, the application moves over to
  it, and then the old one is [deleted](https://docs.strg.com/rest-api/delete-access-key).

## Request

```http
POST /v1/access-keys HTTP/1.1
Host: api.strg.com
Authorization: Bearer <access key id>:<secret>
Content-Type: application/json
Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324

{
  "label": "ci-uploads",
  "permissions": ["objects:list", "objects:read", "objects:write"],
  "bucketScope": ["my-bucket"],
  "keyPrefix": "builds/"
}
```

### Request headers

| Name | Required | Description |
| --- | --- | --- |
| `Authorization` | Yes | `Bearer <access key id>:<secret>` of an access key with the permission *Manage access keys through the API* (`api:access-management`). [Authentication](https://docs.strg.com/rest-api/overview#authentication) in the overview explains it |
| `Content-Type` | Yes | `application/json`. A parameter such as `; charset=utf-8` is fine. Any other type is answered `415 unsupported_media_type` |
| `Idempotency-Key` | No | 1 to 255 printable ASCII characters - a UUID works well. It can be sent bare or as a quoted string (`"8e03978e-…"`); the quotes are not part of the key. [Idempotency](https://docs.strg.com/rest-api/create-access-key#idempotency) explains what it does |

### Request body

A JSON object of at most 64 KiB, with the fields below and no others - a field not listed here is
answered `400 invalid_request`.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | string | Yes | A name for the key, at most 64 characters. Leading and trailing spaces are removed. It cannot be empty, and cannot be `STRG Dashboard` or `STRG Assistant` in any capitalization - those are the names of keys the dashboard creates for itself |
| `permissions` | array of strings | Yes | The permission names, such as `objects:read` - [Permissions](https://docs.strg.com/access-keys#permissions) lists every one. At least one, none twice, and at least one storage permission: a key holding only `mcp:*` or `api:*` permissions would have nothing to reach |
| `bucketScope` | array of strings or `null` | No | The names of the buckets the key is limited to. Omitted or `null`: every bucket in your account, including buckets created later |
| `keyPrefix` | string or `null` | No | The object key prefix the key is limited to, such as `builds/` - matched literally and case-sensitively. Omitted or `null`: no prefix |

These rules decide what a key may hold:

| Rule | Detail |
| --- | --- |
| Always included | `buckets:list` and `buckets:read-config` are part of every key, so they are left out of `permissions` - they are not among the names it takes, and sending one is answered `400 invalid_request` |
| Permissions that need another | `object-retention:bypass-governance` needs `objects:delete-versions` or `object-retention:write`; `bucket-object-lock:write` needs `bucket-object-lock:read`; `mcp:access-management` and `mcp:skip-confirmation` need `mcp:enabled` |
| Permissions that depend on the scope | `buckets:write` and `buckets:delete` need a key for every bucket (`bucketScope` omitted or `null`); `buckets:list-all` needs a key limited to some buckets |
| `bucketScope` | 1 to 100 valid bucket names, none twice. Each is a bucket you own at the time of the request: a name that does not exist yet is answered `422 unknown_bucket` |
| `keyPrefix` | Not empty - leaving the field out is how to ask for no prefix - at most 1,024 bytes, and no control characters |

A key limited to some buckets reaches them by the bucket itself, not just its name: a bucket deleted
and re-created under the same name is a new bucket, and the key no longer reaches it.
[Scope](https://docs.strg.com/access-keys#scope) explains this.

And this is how the key you call with shapes the new one:

| The calling key | The new key |
| --- | --- |
| Holds a set of permissions | Holds only permissions from that set |
| Is limited to some buckets | Is limited to some of those buckets - not to every bucket |
| Is limited to an object prefix | Has a prefix that starts with it |

A request outside these is answered `403 exceeds_parent`. The calling key always holds
`api:access-management`, so the new key may hold it too.

## Response

`201 Created`, with the new key and its secret access key.

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `Location` | `/v1/access-keys/<id>` - the key's own URL, for [Get an access key](https://docs.strg.com/rest-api/get-access-key) |
| `Idempotent-Replayed` | `true` when an earlier request with the same `Idempotency-Key` created the key. Absent otherwise |
| `Cache-Control` | `no-store` |

```json
{
  "id": "STRGEXAMPLEKEYID0001",
  "label": "ci-uploads",
  "bucketScope": ["my-bucket"],
  "keyPrefix": "builds/",
  "permissions": ["objects:list", "objects:read", "objects:write"],
  "origin": "api",
  "createdAt": "2026-10-18T09:30:00.000Z",
  "regions": {},
  "secretAccessKey": "<secret access key>"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | The access key ID - what an S3 client sends as `AWS_ACCESS_KEY_ID` |
| `label` | string | The name, with leading and trailing spaces removed |
| `bucketScope` | array of strings or `null` | The buckets the key reaches, by name - `null` for every bucket |
| `keyPrefix` | string or `null` | The object key prefix the key is limited to, or `null` |
| `permissions` | array of strings | The permissions, as sent |
| `origin` | string | Where the key was created: `api` here. The dashboard's **Access Keys** page tags it *Created through the API* |
| `createdAt` | string | When the key was created - ISO 8601, in UTC |
| `regions` | object | Each region where the key is set up: `pending` until it works there, then `active`. Empty in the answer to a new create; a replay shows how far the key has got by then |
| `secretAccessKey` | string | The secret access key - what an S3 client sends as `AWS_SECRET_ACCESS_KEY` |

- **This response is the moment to store the secret access key.** Only this response carries it -
  and, within the idempotency window, a replay of it. [List access keys](https://docs.strg.com/rest-api/list-access-keys)
  and [Get an access key](https://docs.strg.com/rest-api/get-access-key) never return it. If a secret ever goes missing, a
  new key takes only a moment to create, and deleting the old one makes sure nobody else can use it.
- **A new key takes a moment to get ready.** It works in each region where you have a bucket once
  it is `active` there. [Get an access key](https://docs.strg.com/rest-api/get-access-key)
  shows `regions` as it changes, and
  [When a key starts working](https://docs.strg.com/access-keys#when-a-key-starts-working) describes what the S3
  API answers meanwhile.
- **Keys stay exactly as they were created**, which makes them easy to reason about: a key that
  worked yesterday does the same today. Different permissions, buckets or prefix simply means a
  new key, and the old one can then be [deleted](https://docs.strg.com/rest-api/delete-access-key).

## Idempotency

A create that ends in a timeout, a dropped connection or a `500` may or may not have made the key.
An `Idempotency-Key` takes the guesswork out of that: a request carrying one is remembered for
24 hours, for the key you call with, so sending it again is always safe.

| Within 24 hours, from the same calling key, with the same `Idempotency-Key` | Answer |
| --- | --- |
| The same body | `201` with the key the first request created - the same `id` and `secretAccessKey` - and `Idempotent-Replayed: true`. Nothing new is created |
| The same body, while the first request is still running | Waits for it, then answers as above |
| A different body | `422 idempotency_key_mismatch` |
| The same body, after the key it created was deleted | `409 idempotent_key_revoked` |

- **A retry with the same `Idempotency-Key`** after a timeout, a lost connection or a `500` creates
  at most one key, however often it is sent.
- **A create that was turned down is not remembered.** After a `402` or a `422 unknown_bucket`,
  once the cause is sorted - a card added, the bucket created - the same `Idempotency-Key` creates
  the key.
- *The same body* means the same values: the same `label` and `keyPrefix`, exactly as sent, the
  same entries in `permissions` and `bucketScope` in any order, and `null` the same as an omitted
  field. Spacing and the order of the fields make no difference.
- The same `Idempotency-Key` from another access key, or after 24 hours, is a new request.

## Errors

This table lists every error the operation can answer, so a client can tell them apart by `code`.
Each is an `application/problem+json` body - [Errors](https://docs.strg.com/rest-api/overview#errors) shows its shape.

| Status | `code` | When |
| --- | --- | --- |
| `400` | `invalid_request` | The body is not valid JSON, has a missing, unknown or wrongly typed field, or breaks a rule of [Request body](https://docs.strg.com/rest-api/create-access-key#request-body). Or `Idempotency-Key` is not 1 to 255 printable ASCII characters. `detail` names the problem |
| `401` | `unauthenticated` | No valid `Authorization` header, or the key is unknown, deleted, has another secret, or lacks *Manage access keys through the API*. Every case gets the same answer, with `WWW-Authenticate: Bearer realm="strg.com"` |
| `402` | `payment_method_required` | Your account has no verified card yet. Adding one is something only a person can do, on the dashboard's **Billing** page - [A card is required](https://docs.strg.com/billing#a-card-is-required) explains why |
| `403` | `exceeds_parent` | The new key would have more access than the key you call with: a permission it lacks, a bucket outside its buckets, every bucket when it is limited to some, or a prefix outside its prefix |
| `403` | `account_closed` | The account has been closed |
| `409` | `idempotent_key_revoked` | The key an earlier request with this `Idempotency-Key` created has been deleted since |
| `413` | `payload_too_large` | The body is larger than 64 KiB |
| `415` | `unsupported_media_type` | `Content-Type` is not `application/json` |
| `422` | `unknown_bucket` | `bucketScope` names a bucket you do not have. `detail` lists them |
| `422` | `idempotency_key_mismatch` | This `Idempotency-Key` was already used for a different body |
| `429` | `rate_limited` | Too many requests for this key or from this address. `Retry-After` gives the seconds until the next attempt is welcome |
| `500` | `internal_error` | Something went wrong on our side. A retry with the same `Idempotency-Key` is safe |
| `503` | `buckets_unavailable` | Your buckets could not be checked just now, so `bucketScope` could not be confirmed. `Retry-After` gives the seconds until a retry |

## Examples

Each example creates a key limited to `my-bucket` and the prefix `builds/` that can list, download
and upload, calling with the key in `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. Each one also
retries a failed attempt with the same `Idempotency-Key`, so it never creates two keys.

**curl**

```bash
IDEMPOTENCY_KEY=$(uuidgen)

curl -sS --retry 3 https://api.strg.com/v1/access-keys \
  -H "Authorization: Bearer $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "label": "ci-uploads",
    "permissions": ["objects:list", "objects:read", "objects:write"],
    "bucketScope": ["my-bucket"],
    "keyPrefix": "builds/"
  }'
```

`--retry` resends the same `Idempotency-Key` after a timeout, a `429`, a `500` or a `503`, waiting
as long as `Retry-After` says. Running the `curl` command again with the same `$IDEMPOTENCY_KEY`
gets the same key back.

**Python (requests)**

```python title="create_key.py"
import os
import uuid

import requests

key_id = os.environ["AWS_ACCESS_KEY_ID"]
secret = os.environ["AWS_SECRET_ACCESS_KEY"]

body = {
    "label": "ci-uploads",
    "permissions": ["objects:list", "objects:read", "objects:write"],
    "bucketScope": ["my-bucket"],
    "keyPrefix": "builds/",
}
# One idempotency key for this new access key, reused on every attempt.
idempotency_key = str(uuid.uuid4())

response = None
for attempt in range(3):
    try:
        response = requests.post(
            "https://api.strg.com/v1/access-keys",
            headers={
                "Authorization": f"Bearer {key_id}:{secret}",
                "Idempotency-Key": idempotency_key,
            },
            json=body,
            timeout=30,
        )
    except (requests.ConnectionError, requests.Timeout):
        continue
    if response.status_code < 500:
        break

if response is None:
    raise SystemExit("No answer from the API")
result = response.json()
if not response.ok:
    raise SystemExit(f"{result['status']} {result['code']}: {result['detail']}")

# The secret is in this response only, so this is the moment to store it.
print(result["id"])
print(result["secretAccessKey"])
```

**JavaScript (fetch)**

```javascript title="create-key.mjs"
import { randomUUID } from "node:crypto";

const authorization = `Bearer ${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}`;
const body = JSON.stringify({
  label: "ci-uploads",
  permissions: ["objects:list", "objects:read", "objects:write"],
  bucketScope: ["my-bucket"],
  keyPrefix: "builds/",
});
// One idempotency key for this new access key, reused on every attempt.
const idempotencyKey = randomUUID();

let response;
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    response = await fetch("https://api.strg.com/v1/access-keys", {
      method: "POST",
      headers: {
        Authorization: authorization,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body,
    });
  } catch {
    continue;
  }
  if (response.status < 500) break;
}

if (response === undefined) throw new Error("No answer from the API");
const result = await response.json();
if (!response.ok) throw new Error(`${result.status} ${result.code}: ${result.detail}`);

// The secret is in this response only, so this is the moment to store it.
console.log(result.id);
console.log(result.secretAccessKey);
```

`fetch` sends a string body as `text/plain` unless `Content-Type` is set, which is why the example
sets it - without it, this operation answers `415`.

## Related

- [List access keys](https://docs.strg.com/rest-api/list-access-keys)
- [Get an access key](https://docs.strg.com/rest-api/get-access-key)
- [Delete an access key](https://docs.strg.com/rest-api/delete-access-key)
- [REST API overview](https://docs.strg.com/rest-api/overview) - authentication, errors, rate limits and the OpenAPI document
- [Access keys](https://docs.strg.com/access-keys) - scope, object prefixes and every permission
