Skip to main content

REST API overview

The REST API is how your own code manages access keys: listing them, looking one up, creating one and deleting one, without a person clicking through the dashboard. It suits the moments when keys come and go with your automation - a CI pipeline that creates a key limited to one bucket for a deploy and deletes it when the job is done, a scheduled job that rotates a service's key by creating a new one and deleting the old, or a tool of yours that hands each new project a key of its own. Access keys are all it manages; buckets and objects are the S3-compatible API's work.

Operations​

These are the operations the API answers, each with a page of its own:

OperationRequest
List access keysGET /v1/access-keysList access keys
Get an access keyGET /v1/access-keys/{id}Get an access key
Create an access keyPOST /v1/access-keysCreate an access key
Delete an access keyDELETE /v1/access-keys/{id}Delete an access key
The OpenAPI documentGET /v1/openapi.jsonOpenAPI document

Endpoint and versioning​

Every request goes to one base URL, and speaks JSON both ways:

Base URLhttps://api.strg.com/v1/
Request bodiesJSON, sent with Content-Type: application/json, at most 64 KiB. A field the operation does not define is answered 400 invalid_request, so a misspelt field name shows up straight away rather than being silently dropped
ResponsesJSON (application/json), errors as application/problem+json - Errors has the details. Every response carries Cache-Control: no-store
TimesISO 8601, in UTC - 2026-10-18T09:30:00.000Z

Within /v1, fields are only ever added - never removed, renamed or changed in type. A client that ignores response fields it does not know keeps working, unchanged, each time we add one.

The API sends no CORS headers, so a web page in a browser cannot call it. It belongs on a server, in a script or in a CI job, where the key's secret stays out of the page's reach.

Authentication​

Every request but the OpenAPI document is made with an access key that holds Manage access keys through the API (api:access-management), sent as a bearer token of its id and secret joined by a colon:

Authorization: Bearer <access key id>:<secret>

Such a key is created on the Access Keys page of the dashboard (https://app.strg.com/access-keys). The Full Access template already includes the permission; with any other template, it is the Manage access keys through the API box under REST API. Like every key, it also carries at least one storage permission - Access keys explains them all.

A first request, listing one key, looks like this:

curl -sS "https://api.strg.com/v1/access-keys?limit=1" \
-H "Authorization: Bearer $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY"

Every authentication failure is the same 401, with code unauthenticated and WWW-Authenticate: Bearer realm="strg.com": a missing or malformed header, an unknown key id, a wrong secret, a deleted key, a key without Manage access keys through the API, or a closed account. Keeping them alike means the answer tells someone guessing at keys nothing about which part they got right.

What the key reaches​

The calling key's own buckets, prefix and permissions matter differently for reading and deleting keys than for creating them:

OperationReaches
List, get, deleteEvery access key of your account, whatever its buckets, prefix and permissions - the calling key included. The keys STRG manages for you stay out of it: the dashboard's own key and the assistant's are not listed, and answer 404
CreateA new key never has more access than the calling key: permissions the calling key has, buckets it can reach - all buckets when it is for all buckets itself - and, when it has an object prefix, a prefix that starts with it. A request asking for more is answered 403 exceeds_parent
This permission reaches every key

A key with Manage access keys through the API can delete every other key in your account - even a key limited to one bucket can delete a key for all buckets. A deleted key stops working within 60 seconds, everywhere, and cannot be restored. This permission belongs with a script or tool you trust with all of your keys.

A key can delete itself, like any other: its requests stop being accepted within 60 seconds.

Creating a key needs a verified card on your account, just as in the dashboard. Adding a card is a person's step, on the Billing page; Billing explains why.

Errors​

Every error is an RFC 9457 problem, sent as application/problem+json, so a client reads every failure the same way:

{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"code": "exceeds_parent",
"detail": "Delete buckets is not a permission of the key you are using - a new key cannot have more access than it."
}

Each field of the body has its own job:

Field
typeAlways about:blank
titleThe HTTP status's own phrase
statusThe HTTP status code
codeWhat your client branches on - stable, from the table below
detailA sentence for a person, naming the field or the rule. Its wording can change, so it is meant for reading rather than parsing

These are every code the API answers, with its status:

codeStatusMeaning
invalid_request400The request is not valid: the body is not JSON, a field is missing, unknown or of the wrong type, the key asked for breaks a rule of Access keys - no storage permission, a name over 64 characters, a permission that needs another or a different scope - limit is outside 1-100, or Idempotency-Key is malformed. detail says which
invalid_cursor400cursor is not one a listing returned
unauthenticated401Authentication covers every case
payment_method_required402Creating a key needs a verified card on your account, which a person adds on the Billing page - Billing has the details
exceeds_parent403The new key would have more access than the calling key - What the key reaches explains the rule
account_closed403The account has been closed
not_found404No such endpoint, or no such access key: an unknown id, a key deleted already, or a key STRG manages for you
method_not_allowed405The endpoint does not answer this method. The Allow header lists those it does
idempotent_key_revoked409The access key an earlier request with this Idempotency-Key created has been deleted since - Idempotency has the details
payload_too_large413The request body is larger than 64 KiB
unsupported_media_type415The request body was not sent as application/json
unknown_bucket422bucketScope names a bucket you do not have. A key is limited to buckets that exist
idempotency_key_mismatch422This Idempotency-Key was already used for a different request - Idempotency has the details
rate_limited429Too many authentication attempts - Rate limits explains them
internal_error500Something went wrong on our side. A retry in a moment usually succeeds - with the same Idempotency-Key for a create
buckets_unavailable503Your buckets could not be checked just now, to confirm a bucketScope. A retry after the Retry-After header's seconds usually succeeds

Rate limits​

Authentication is limited per access key and per client address, which keeps anyone from guessing secrets at speed. Over either limit, a request is answered 429 with code rate_limited and a Retry-After header: after that many seconds, requests are accepted again.

Paging​

GET /v1/access-keys answers one page at a time, newest first, steered by these values:

limitQuery parameter: keys per page, 1 to 100. Default 100
cursorQuery parameter: the previous page's nextCursor. Left out for the first page
nextCursorIn the answer: passed as cursor, it fetches the next page. null on the last page

Paging is stable while keys are created or deleted: a key created or deleted between two requests never makes another key appear twice or be skipped. A cursor is opaque - it works exactly as returned, and is not something a client builds itself.

A complete listing walks the pages until nextCursor is null:

This loop uses jq to read the answer.

cursor=""
while :; do
page=$(curl -sS -G https://api.strg.com/v1/access-keys \
-H "Authorization: Bearer $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
--data-urlencode "limit=100" \
${cursor:+--data-urlencode "cursor=$cursor"})
echo "$page" | jq -r '.accessKeys[] | "\(.id)\t\(.label)"'
cursor=$(echo "$page" | jq -r '.nextCursor // empty')
[ -z "$cursor" ] && break
done

Idempotency​

A create that ends without a clear answer - a timeout, a dropped connection, a 500 - may or may not have made the key. An Idempotency-Key header takes the guesswork out: a retry carrying the same value is sure to leave exactly one key, and hands it back.

idempotency_key=$(uuidgen)
curl -sS -i 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-deploy", "permissions": ["objects:list", "objects:read", "objects:write"], "bucketScope": ["my-bucket"]}'

How a value is remembered, and what a second request with it gets:

The value1 to 255 printable ASCII characters - a UUID works well. It can also be sent as a quoted string ("…")
Window24 hours from the first request. After that, the same value creates a new key
Bound toThe access key that sent it. The same value from another key is a different request
A replayThe same request within the window creates nothing and answers 201 with the key the first request created, its secret included, and Idempotent-Replayed: true
Two at onceA second request with the same value, sent while the first is still running, waits for it and answers the same key
A different body422 idempotency_key_mismatch. A new request gets a new value
The key deleted since409 idempotent_key_revoked - there is nothing left to hand back, and a new value creates another
An unsuccessful createNot remembered. Once the cause is fixed - a card added, say - a retry with the same value creates the key

Without the header, every create makes a new key.

OpenAPI document​

https://api.strg.com/v1/openapi.json describes every operation, parameter, body and error code as OpenAPI 3.1. It needs no key, so a client generator or an API tool can read it directly.

View as Markdown