Bucket control API
Manage private buckets, console objects, scoped access keys, app connections and temporary download links with your Fortly account token.
These feature-gated endpoints use Authorization: Bearer $FORTLY_TOKEN on https://api.fortly.com. They do not accept S3 keys. Object requests through https://s3.fortly.com use SigV4 and bucket-scoped keys instead. Read the private bucket guide for access and compatibility limits.
Resource endpoints
All paths below begin with /projects/{name}/buckets. Use the project’s full name, such as my-app-ada. Bucket IDs come from list/create responses; never substitute the S3 bucket name for a control-plane ID.
| Method | Suffix | Purpose |
|---|---|---|
| GET | (none) | Feature flags, capabilities, buckets, aggregate usage and account pool |
| POST | (none) | Create a private bucket |
| GET | /{bucket} | Bucket settings, usage, capabilities, endpoint, region and limits |
| PATCH | /{bucket} | Update display name and browser-access rules with revision check |
| DELETE | /{bucket} | Confirm permanent deletion and revoke access before cleanup |
| GET | /{bucket}/objects?prefix=&cursor= | Current folder, at most 100 objects/prefixes |
| PUT | /{bucket}/objects?key= | Upload raw bytes, at most 64 MiB |
| DELETE | /{bucket}/objects?key= | Delete one object |
| POST | /{bucket}/download | Generate a temporary GET link |
| DELETE | /{bucket}/download-links | Revoke all console-generated links |
| GET | /{bucket}/delivery/analytics?window=24h | Bounded cache/traffic/timing summaries and time buckets |
| GET | /{bucket}/delivery/requests?window=24h | Filtered request records, 50 per page with opaque cursor |
| GET | /{bucket}/delivery/requests/{request} | Inspect one opaque request ID in this bucket |
| GET / PUT | /{bucket}/delivery | Inspect delivery / save access, cache lifetime and revision |
| GET | /{bucket}/delivery/preview?prefix= | Preview the exact folder before publication |
| POST | /{bucket}/delivery/links | Create a scoped public URL or temporary signed CDN link |
| POST | /{bucket}/delivery/purge | Start a fresh cache generation across edges |
| GET / POST | /{bucket}/keys | Safe key summaries / one-time key creation result |
| DELETE | /{bucket}/keys/{key} | Revoke a key |
| GET / POST | /{bucket}/connections | List connections / configure the project’s app |
| POST | /{bucket}/connections/{connection}/rotate | Install replacement app credentials, retaining the previous key for explicit retirement |
| DELETE | /{bucket}/connections/{connection} | Revoke connection key and remove unchanged owned variables |
Metadata visibility follows existing project membership. Private object and administration actions require the owner/admin capability. Every bucket must belong to the exact immutable project/account being requested. Public Project files remain on their original API routes.
Create safely
Bucket creation, access-key creation and app connection require an Idempotency-Key containing 1–80 ASCII letters, numbers, hyphens or underscores. Generate a new value for each intended operation and reuse it when retrying that operation. Reusing a key with different input returns a conflict.
curl https://api.fortly.com/projects/my-app-ada/buckets \
-H "Authorization: Bearer $FORTLY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-uploads-001" \
-d '{"display_name":"App uploads"}'The response includes bucket.id, the immutable bucket.name for S3, display_name, environment_id, state and revision. The environment is currently default. New provisioning can be paused while existing buckets remain available.
Update and delete
PATCH requires the latest revision and at least one of display_name or cors. Omitted settings stay unchanged: a name-only update preserves browser-access rules, and a CORS-only update preserves the display name. An explicit cors: [] removes all browser-access rules. Null values, unknown settings, missing revisions and invalid field types return 400 with JSON error and code fields.
A name-only update looks like:
{
"display_name": "Renamed uploads",
"revision": 1
}A stale revision returns 409 without changing either setting; reload the bucket before resubmitting. Each successful update increments the revision. The stable S3 bucket name never changes. To update both settings in one request, send:
{
"display_name": "App uploads",
"revision": 1,
"cors": [{
"origins": ["https://app.example.com"],
"methods": ["GET", "HEAD"],
"headers": ["authorization", "x-amz-date", "x-amz-content-sha256"],
"max_age": 600
}]
}Use cors: [] to disable browser access. At most four rules, ten origins per rule, five methods, 32 explicit headers and one hour of preflight caching are allowed. No wildcard origins or cookies.
DELETE requires {"confirmation":"App uploads"} matching the display name. It returns state: "deleting" after access has been revoked. Cleanup is asynchronous and permanent; the bucket disappears from ordinary listings when complete.
Objects and temporary links
Encode the complete object key as one query value. Do not normalize its slashes or decode percent escapes yourself. Listings return objects, prefixes and next_cursor. Cursors expire after one day and are bound to bucket, prefix and delimiter. They cannot be reused for another folder.
PUT accepts the file bytes as the body and a supported Content-Type. It returns the object metadata. Deletion is idempotent for missing keys but refuses while another upload owns the destination lease.
POST /download accepts {"key":"exports/report.pdf","seconds":900}. Duration defaults to 900 seconds and must be between 60 and 86,400. The result is { "url": "…", "expires_at": … }. Treat the URL as a bearer secret. It follows the object key until expiry, including replacements.
Access keys
POST /keys accepts name, permission (read or read_write), optional Unix expires_at within one year, and optional replacement_for naming an existing manual key. It returns { "key": { …safe summary… }, "secret": "…" } once. Ordinary GET responses contain no signing material.
A replacement leaves the original active until explicitly revoked. Key creation requires an idempotency key. Repeating it returns a conflict naming the existing key, without creating a duplicate or revealing its secret again. If the creation response was lost, revoke that listed key and deliberately create a replacement with a new request ID.
Key revocation is checked on each S3 request and before publishing an upload. It invalidates SDK-signed URLs from that key. Console download links use a separate signer; revoke those with DELETE /download-links.
App connections
POST /connections takes permission and optional variable_prefix, with an idempotency key. The target is the project’s current app and Default environment. A prefix accepts up to 24 uppercase letters, digits or underscores, starting with a letter or underscore.
Connection responses return the canonical prefix, including its separator: PHOTOS and PHOTOS__ both return PHOTOS_ and save PHOTOS_S3_BUCKET, for example. An empty prefix uses the default names. Do not append another underscore to a returned prefix. The console's JavaScript examples use the saved prefix and are executed in the gateway integration tests.
The dedicated key, five encrypted runtime variables and connection record commit together. Existing variable names return 409 variable_conflict; no partial connection is installed. The response reports configuration saved and any deployment consequence. Rotation requires a new idempotency key and updates the dedicated runtime credentials together. The previous key stays active until explicit retirement; verify the app, then revoke its returned old_key_id. Disconnect revokes all keys belonging to the connection and removes only unchanged runtime variables it owns; preserved_variables names customer-edited values left alone.
Connection listing returns up to 100 records, prioritizing connections with valid current credentials, then other connected records, then recent disconnected history. This keeps current app credentials visible after repeated connection changes. It is a bounded operational list, not a complete audit export. Disconnect relinquishes variable ownership so later cleanup cannot remove a replacement connection's configuration.
Creation and protected writes recheck current account state and owner/admin membership inside their transaction, including idempotent creation retries. Removing a teammate does not revoke their previously issued S3 credentials; an owner/admin must explicitly revoke those keys or rotate and retire the affected app credentials. See offboarding access.
CDN delivery
GET /{bucket}/delivery requires private-data read permission and reports readiness, saved settings, current UTC-month bucket activity, shared account usage and configured operational budgets. It contains no signing secret. PUT takes {mode, prefix, ttl, revision, confirmation}: mode is disabled, signed or public; cache lifetime is 60–86400 seconds. Prefixes are empty or end in /; public mode requires a nonempty folder. New or changed public folders requires the exact bucket display name in confirmation. Settings use optimistic revision checks and current owner/admin authorization; disabling remains possible if delivery rollout is paused.
GET /delivery/preview?prefix=public%2F requires private-data read permission and returns folder count, bytes and up to five sample keys for explicit publication review. POST /delivery/links takes {key, seconds} and returns a public URL for an already-published folder, or a signed exact-object link with bounded expiry. This management endpoint requires the current owner/admin. Apps can generate private CDN tokens on the server using their existing bucket-scoped S3 read key and the documented helper.
POST /delivery/purge advances cache generation; subsequent requests at all edges select a fresh cache identity. Old physical entries expire later. Changing settings also changes generation. Pausing blocks new requests, including cache hits, without removing objects or S3 credentials. Console link revocation retires both console S3 and signed CDN links; app keys and public URLs are unaffected.
Delivery traffic and budgets are separate from S3/console object totals. The account/month ledger reserves a request and expected response bytes before serving a hit or miss. Missing completion receipts retain reservations; over-budget requests fail. These budgets do not create a price, payment collection or prepaid balance integration. See delivery behavior and limits.
Errors and limits
Control errors use JSON { "error": "Actionable message", "code": "machine_code" }. JSON decoding and schema errors use code: "invalid_request": malformed JSON and missing or incorrectly typed fields return 400, an oversized JSON body returns 413, and a missing or unsupported JSON content type returns 415. Set Content-Type: application/json for JSON requests; raw object uploads still send their file content type.
Other expected statuses include 403 private-data permission or quota, 404 unresolved resource, 409 conflict, 429 concurrent transfers and 503 unavailable storage. An installation with the feature disabled returns enabled: false from its bucket list; bucket-specific routes are unavailable.
Committed bytes, incomplete multipart bytes, outstanding reservations and pending deletion bytes remain charged to the account’s shared pool. Overwrites need temporary room for both versions. Reads and deletions remain available over quota. See capacity and compatibility.
Bucket detail also returns traffic: since (UTC month start, Unix seconds), requests, read_requests, write_requests, failed_requests, incomplete_requests, received_bytes, and sent_bytes. Totals follow the UTC month each request started. These are durable counts of supported authorized data-plane attempts and HTTP body bytes processed by Fortly, not invoices or confirmed client receipts. They exclude control-plane settings/key administration, rejected authentication and requests targeting a different bucket. They include console object operations and S3 requests, with SDK retries and multipart parts counted separately. A request is incomplete until its declared response length has been handed to the HTTP transport, or an unknown-length stream reaches EOF; interrupted streams retain their recorded progress.
Plan reductions and pack removals are rejected when the requested storage pool cannot hold committed bytes and reservations. No data is deleted and the current plan stays unchanged. There is no payment collection or prepaid balance integration in these endpoints.
Delivery observability
These endpoints require a current owner/admin account; neither metadata-only membership, S3 keys nor an MCP project-read grant permits request inspection. Responses use private/no-store caching and contain no object path, signed URL, IP or signing material.
GET /delivery/analytics?window=24h accepts 1h, 24h, 7d or 30d. It returns source: "admitted_requests", UTC since/until, bucket_seconds, totals, zero-filled series, and reason/HTTP-status/top-location breakdowns. Timings are milliseconds; timestamps are Unix seconds. Missing historical telemetry is null. Summaries use a consistent, read-only snapshot with bounded query time; a busy query returns 503 and a shorter-window suggestion.
GET /delivery/requests accepts the same window and optional outcome (all, hit, miss, bypass, errors, pending), reason (a documented reason code or unmeasured), numeric HTTP status, location (a three-letter location code or Unmeasured) and cursor. It returns at most 50 records and next_cursor. Treat the cursor as opaque; it is bound to the bucket, window and filters. Do not reuse it after changing them. Filters apply to this request endpoint, not summary charts. The console caps displayed records at 500; API pages remain available within the bounded window.
GET /delivery/requests/{request} retrieves one cdn_ opaque ID from the bucket, including older retained records outside the selected chart window. A foreign/missing request is 404; malformed IDs/filters are 400. Unknown query parameters are refused.
Records include id, started_at, completed_at, cache_status, cache_reason, final_status, expected_bytes, processed_bytes, optional admission_ms, first_byte_ms, total_ms and edge_location. Completed outcome/timing fields do not imply client acknowledgement. Pending records retain their reserved bytes. Rejections before admission are absent; never present these totals as all edge traffic or zero edge errors. See coverage and cache checks.