Skip to content

Platform controls API

Configure regional delivery, image focal points, bulk migrations, delivery domains, encryption of originals, and single sign-on.

On this page

These routes control how a whole workspace behaves: where files may be delivered, how images crop, how large libraries move in, which hostnames serve files, whether originals are encrypted, and how people sign in. Most of them are workspace administration, so read Access before giving an integration a key for them.

Access#

Every route under /api/platform/ follows the workspace-administration scopes for API keys: GET needs workspace:read, and every other method needs workspace:admin. This applies even to routes that change a single file, such as the focal point.

Route groupAPI key scopeSigned-in role
/api/platform/delivery-policyworkspace:read to read, workspace:admin to changeOwner or admin
/api/platform/domainsSameOwner or admin
/api/platform/ssoSameOwner or admin
/api/platform/webhooksSameOwner or admin
/api/platform/assets/{asset_id}/focal-pointworkspace:adminMember or above
/api/platform/migrationsworkspace:read to list, workspace:admin to createAny member to list, member or above to create
Encryption settingNot available to API keysOwner or admin

Regional delivery policy#

Restrict which countries can receive your files, and read the storage region the workspace uses.

Get the delivery policy#

GET/api/platform/delivery-policy
Requiresworkspace:read
curl https://api.steadylink.io/api/platform/delivery-policy \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "mode": "all",
  "countries": [],
  "storageRegion": "auto",
  "availableStorageRegions": ["auto"]
}

storageRegion is where originals are stored. Each deployment currently offers one region, listed in availableStorageRegions.

Set the delivery policy#

PUT/api/platform/delivery-policy
Requiresworkspace:admin

Body

modestringDefault all
all delivers everywhere. allow delivers only to the listed countries. deny delivers everywhere except the listed countries.
countriesstring[]
Two-letter ISO 3166 codes, case-insensitive, up to 249. At least one is required for allow and deny. Duplicates are removed and the list is sorted.
storageRegionstring
Optional. Must equal the current region; any other value returns 422 with "code": "unsupported_storage_region".
curl -X PUT https://api.steadylink.io/api/platform/delivery-policy \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "deny", "countries": ["KP", "RU"] }'
200 OKResponse
{ "mode": "deny", "countries": ["KP", "RU"], "storageRegion": "auto", "availableStorageRegions": ["auto"] }

A blocked delivery request receives 451 ("This asset is not available in your region"). The policy fails closed: with allow or deny active, a delivery request is served only when it arrives through SteadyLink's edge, which attaches the visitor's country. A request that reaches delivery any other way is refused with 451, even from a permitted country. Policy changes can take a short time to reach every delivery server, because the policy is cached briefly.

Focal points#

A focal point is the part of an image that must stay visible when a transformation crops it. Set it once per file; every delivery URL that crops with fit=cover then keeps that point in frame, including URLs already in use once their cached copies expire.

Set a focal point#

PUT/api/platform/assets/{asset_id}/focal-point
Requiresworkspace:admin

Body

xnumberRequired
Horizontal position from 0 (left edge) to 1 (right edge). Rounded to four decimals.
ynumberRequired
Vertical position from 0 (top edge) to 1 (bottom edge).
curl -X PUT https://api.steadylink.io/api/platform/assets/3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44/focal-point \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "x": 0.42, "y": 0.31 }'
200 OKResponse
{ "x": 0.42, "y": 0.31 }

The stored point applies only when a delivery URL uses fit=cover, has no explicit fp-x and fp-y (or focal-x and focal-y), and does not set focus=auto. Explicit URL parameters always win, so one URL can still override the stored point. Values outside 0 to 1 return 422. The point belongs to the file, not to a revision, so it stays in place when you replace the file; set it again if the new image is composed differently. See Image transformations for the URL parameters.

Clear a focal point#

DELETE/api/platform/assets/{asset_id}/focal-point
Requiresworkspace:admin

Removes the stored point and returns 204 No Content. Cover crops go back to centering.

Bulk migrations#

A migration is an upload batch of up to 100 files that keeps each file's folder path from your manifest, plus a record you can list later. Use it to move a library from another storage service while preserving its folder structure. For importing media from a live website by URL, use website migration instead.

Create a migration#

POST/api/platform/migrations
Requiresworkspace:admin

Body

bucketIduuidRequired
Destination bucket.
filesobject[]Required
From 1 to 100 files, each with filename (required), size in bytes (required), path (folder inside the bucket, default empty), and contentType (default application/octet-stream).
curl -X POST https://api.steadylink.io/api/platform/migrations \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
    "files": [
      { "filename": "hero.jpg", "path": "2024/spring/", "contentType": "image/jpeg", "size": 734002 },
      { "filename": "catalog.pdf", "path": "2024/print/", "contentType": "application/pdf", "size": 5242880 }
    ]
  }'
201 CreatedResponse
{
  "id": "8f3a1c6e-2d94-4b70-a5e8-0c7b9d2f4e13",
  "status": "uploading",
  "uploadBatch": {
    "id": "1e6d9b42-7c3a-4f85-9e01-5b2c8a7d0f36",
    "status": "active",
    "totalFiles": 2,
    "files": [
      {
        "id": "5a0c7e93-1b4d-4862-b9f7-3e2a6d8c1f50",
        "filename": "hero.jpg",
        "path": "2024/spring/",
        "expectedSize": 734002,
        "status": "created",
        "uploadUrl": "<presigned PUT URL>",
        "...": "other upload session fields"
      }
    ]
  }
}

Then, for each file, PUT its bytes to uploadUrl and complete its upload session, exactly as in the Uploads API. When every file in the batch has finished, the migration becomes complete, or partial if any file failed or was blocked by the scan, and a migration.completed webhook event is sent. Each file is checked against the plan's upload size and storage limits when the migration is created. For more than 100 files, create several migrations.

List migrations#

GET/api/platform/migrations
Requiresworkspace:read
curl https://api.steadylink.io/api/platform/migrations \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "items": [
    {
      "id": "8f3a1c6e-2d94-4b70-a5e8-0c7b9d2f4e13",
      "bucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
      "uploadBatchId": "1e6d9b42-7c3a-4f85-9e01-5b2c8a7d0f36",
      "status": "complete",
      "totalFiles": 2,
      "createdAt": "2026-10-08T13:00:00.000000"
    }
  ]
}

Returns the 50 most recent migrations. A migration still in progress shows created here.

Delivery domains#

Serve files from your own hostname, such as files.northwind.example, instead of cdn.steadylink.io. Adding a domain is a two-part process: you prove you own it with a DNS TXT record, then SteadyLink completes the delivery setup for that hostname. The API covers ownership; deliveryReady stays false and edgeStatus reads manual_setup_required until the setup is finished on SteadyLink's side.

The domain object#

Domain
{
  "id": "4d8e2a17-9b3c-4f60-a1d5-7e0c3b9f2a84",
  "hostname": "files.northwind.example",
  "status": "pending",
  "verification": {
    "type": "TXT",
    "name": "_steadylink.files.northwind.example",
    "value": "sl-domain=Qm3x..."
  },
  "cnameTarget": "cdn.steadylink.io",
  "verifiedAt": null,
  "edgeStatus": "waiting_for_ownership",
  "deliveryReady": false,
  "createdAt": "2026-10-08T13:10:00.000000"
}

status is pending until verification succeeds, then verified. Point the hostname's CNAME at cnameTarget.

List delivery domains#

GET/api/platform/domains
Requiresworkspace:read
curl https://api.steadylink.io/api/platform/domains \
  -H "X-API-Key: $STEADYLINK_API_KEY"

Returns { "items": [...] } with every domain, oldest first.

Add a delivery domain#

POST/api/platform/domains
Requiresworkspace:admin

Send the bare hostname, without a protocol or path.

curl -X POST https://api.steadylink.io/api/platform/domains \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hostname": "files.northwind.example" }'

Returns the domain object with status 201 Created. Create the TXT record from verification at your DNS provider. Errors: 422 with "code": "invalid_delivery_domain" for a URL, an IP address, or a steadylink.io hostname; 409 with "code": "delivery_domain_claimed" if any workspace already added that hostname.

Verify a delivery domain#

POST/api/platform/domains/{domain_id}/verify
Requiresworkspace:admin

Looks up the TXT record and marks the domain verified if the value matches.

curl -X POST https://api.steadylink.io/api/platform/domains/4d8e2a17-9b3c-4f60-a1d5-7e0c3b9f2a84/verify \
  -H "X-API-Key: $STEADYLINK_API_KEY"

Returns the updated domain. If the record is not visible yet, the response is 409 with "code": "domain_verification_pending" and the expected record. DNS changes can take minutes to hours to propagate, so retry with a growing delay rather than in a tight loop.

Remove a delivery domain#

DELETE/api/platform/domains/{domain_id}
Requiresworkspace:admin

Returns 204 No Content. The hostname becomes available to claim again.

Encryption of originals#

When encryption is on, SteadyLink encrypts each newly committed original before it is stored. Revisions committed earlier are not re-encrypted, and turning the setting off does not decrypt anything. The workspace object reports the current state as encryptionEnabled.

Turn encryption on or off#

POST/api/orgs/{workspace_id}/encryption?enabled=true
Signed-in session (owner or admin)

The setting is a query parameter, not a JSON body. Send enabled=false to turn it off.

curl
curl -X POST "https://api.steadylink.io/api/orgs/6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3/encryption?enabled=true" \
  -H "Authorization: Bearer $STEADYLINK_ACCESS_TOKEN"
200 OKResponse
{ "orgId": "6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3", "encryptionEnabled": true }

Returns 503 when encryption is not configured on the server. Encrypted files cannot be used as a conversion source.

Single sign-on#

Connect an OpenID Connect provider (Okta, Microsoft Entra ID, Google Workspace, or any provider with a discovery document) so people sign in with their company account. SSO requires the Business plan or above; other plans get 403 with "code": "business_plan_required". For the full setup, see Single sign-on.

Get the SSO configuration#

GET/api/platform/sso
Requiresworkspace:read

Returns { "configured": false, "available": true } before setup, where available says whether the plan includes SSO. Once configured, it also returns issuer, clientId, emailDomains, enabled, enforce, testedAt, startPath, and callbackPath. The client secret is never returned.

Configure SSO#

PUT/api/platform/sso
Requiresworkspace:admin

Body

issuerstringRequired
The provider's HTTPS issuer URL. SteadyLink reads /.well-known/openid-configuration from it at sign-in.
clientIdstringRequired
The client ID from your provider.
clientSecretstring
8 to 4096 characters. Required the first time; omit it later to keep the stored secret. Stored encrypted.
emailDomainsstring[]Required
From 1 to 25 domains, such as northwind.example. Only people whose verified email is on one of these domains can sign in.
enabledbooleanDefault false
Turn the connection on.
enforcebooleanDefault false
Require SSO for every member. Only allowed after a successful test login.

In your provider, register the redirect URI https://api.steadylink.io/api/platform/sso/callback.

curl -X PUT https://api.steadylink.io/api/platform/sso \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "issuer": "https://northwind.okta.com",
    "clientId": "0oa8x2k4mQ7",
    "clientSecret": "'"$OKTA_CLIENT_SECRET"'",
    "emailDomains": ["northwind.example"],
    "enabled": true,
    "enforce": false
  }'
200 OKResponse
{
  "configured": true,
  "issuer": "https://northwind.okta.com",
  "clientId": "0oa8x2k4mQ7",
  "emailDomains": ["northwind.example"],
  "enabled": true,
  "enforce": false,
  "testedAt": null,
  "startPath": "/api/platform/sso/6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3/start",
  "callbackPath": "/api/platform/sso/callback"
}

Enforcement is a three-step sequence, and the API rejects shortcuts:

  1. Save and enable

    PUT the configuration with "enabled": true and "enforce": false.

  2. Test the login

    Open https://api.steadylink.io followed by startPath in a browser and sign in with an account on one of the email domains. A successful sign-in sets testedAt.

  3. Enforce

    PUT the same configuration with "enforce": true. Without a successful test, this returns 409 with "code": "sso_test_required".

Changing the issuer, client ID, email domains, or secret clears testedAt. If SSO is already enforced, that PUT fails with 409 until you test again, so a broken configuration cannot lock everyone out. Disabling the connection ("enabled": false) also turns enforcement off.

While SSO is enforced, a member who signed in another way gets 403 with "code": "sso_required" and an ssoUrl on every workspace request. API keys are not affected. People who sign in through SSO for the first time join the workspace as members, if a seat is available. Invalid issuers or domains return 422 with "code": "invalid_sso_configuration".

Webhook registration#

Webhook endpoints are registered under /api/platform/webhooks with the same scopes as the rest of this page. Payloads, event types, signatures, Slack, Discord, and Teams destinations, and retries are documented on Webhooks.

MethodRoutePurpose
GET/api/platform/webhooksList endpoints and the available event types
POST/api/platform/webhooksRegister an endpoint
POST/api/platform/webhooks/{webhook_id}/testSend a test event now
GET/api/platform/webhooks/{webhook_id}/deliveriesRecent delivery attempts
DELETE/api/platform/webhooks/{webhook_id}Remove an endpoint

Next steps#