Platform controls API
Configure regional delivery, image focal points, bulk migrations, delivery domains, encryption of originals, and single sign-on.
On this page
- Access
- Regional delivery policy
- Get the delivery policy
- Set the delivery policy
- Focal points
- Set a focal point
- Clear a focal point
- Bulk migrations
- Create a migration
- List migrations
- Delivery domains
- The domain object
- List delivery domains
- Add a delivery domain
- Verify a delivery domain
- Remove a delivery domain
- Encryption of originals
- Turn encryption on or off
- Single sign-on
- Get the SSO configuration
- Configure SSO
- Webhook registration
- Next steps
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 group | API key scope | Signed-in role |
|---|---|---|
/api/platform/delivery-policy | workspace:read to read, workspace:admin to change | Owner or admin |
/api/platform/domains | Same | Owner or admin |
/api/platform/sso | Same | Owner or admin |
/api/platform/webhooks | Same | Owner or admin |
/api/platform/assets/{asset_id}/focal-point | workspace:admin | Member or above |
/api/platform/migrations | workspace:read to list, workspace:admin to create | Any member to list, member or above to create |
| Encryption setting | Not available to API keys | Owner or admin |
Regional delivery policy#
Restrict which countries can receive your files, and read the storage region the workspace uses.
Get the delivery policy#
/api/platform/delivery-policyworkspace:readcurl https://api.steadylink.io/api/platform/delivery-policy \
-H "X-API-Key: $STEADYLINK_API_KEY"const policy = await steadylink.getDeliveryPolicy();policy = client.get_delivery_policy(){
"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#
/api/platform/delivery-policyworkspace:adminBody
modestringDefaultallalldelivers everywhere.allowdelivers only to the listed countries.denydelivers everywhere except the listed countries.countriesstring[]- Two-letter ISO 3166 codes, case-insensitive, up to 249. At least one is required for
allowanddeny. Duplicates are removed and the list is sorted. storageRegionstring- Optional. Must equal the current region; any other value returns
422with"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"] }'await steadylink.setDeliveryPolicy({ mode: "deny", countries: ["KP", "RU"] });client.set_delivery_policy({"mode": "deny", "countries": ["KP", "RU"]}){ "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#
/api/platform/assets/{asset_id}/focal-pointworkspace:adminBody
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 }'await steadylink.setFocalPoint("3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44", 0.42, 0.31);client.set_focal_point("3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44", 0.42, 0.31)steadylink focal 3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44 --x 0.42 --y 0.31{ "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#
/api/platform/assets/{asset_id}/focal-pointworkspace:adminRemoves 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#
/api/platform/migrationsworkspace:adminBody
bucketIduuidRequired- Destination bucket.
filesobject[]Required- From 1 to 100 files, each with
filename(required),sizein bytes (required),path(folder inside the bucket, default empty), andcontentType(defaultapplication/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 }
]
}'const migration = await steadylink.createMigration("9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10", [
{ filename: "hero.jpg", path: "2024/spring/", contentType: "image/jpeg", size: 734002 },
{ filename: "catalog.pdf", path: "2024/print/", contentType: "application/pdf", size: 5242880 },
]);from steadylink import UploadFile
migration = client.create_migration("9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10", [
UploadFile("hero.jpg", 734002, "image/jpeg", "2024/spring/"),
UploadFile("catalog.pdf", 5242880, "application/pdf", "2024/print/"),
]){
"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#
/api/platform/migrationsworkspace:readcurl https://api.steadylink.io/api/platform/migrations \
-H "X-API-Key: $STEADYLINK_API_KEY"const { items } = await steadylink.listMigrations();items = client.list_migrations()["items"]{
"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#
{
"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#
/api/platform/domainsworkspace:readcurl https://api.steadylink.io/api/platform/domains \
-H "X-API-Key: $STEADYLINK_API_KEY"const { items } = await steadylink.listDeliveryDomains();items = client.list_delivery_domains()["items"]Returns { "items": [...] } with every domain, oldest first.
Add a delivery domain#
/api/platform/domainsworkspace:adminSend 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" }'const domain = await steadylink.addDeliveryDomain("files.northwind.example");domain = client.add_delivery_domain("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#
/api/platform/domains/{domain_id}/verifyworkspace:adminLooks 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"await steadylink.verifyDeliveryDomain("4d8e2a17-9b3c-4f60-a1d5-7e0c3b9f2a84");client.verify_delivery_domain("4d8e2a17-9b3c-4f60-a1d5-7e0c3b9f2a84")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#
/api/platform/domains/{domain_id}workspace:adminReturns 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#
/api/orgs/{workspace_id}/encryption?enabled=trueThe setting is a query parameter, not a JSON body. Send enabled=false to turn it off.
curl -X POST "https://api.steadylink.io/api/orgs/6e0b2f7a-1c94-4d3b-a8e5-2f71c0d9b6a3/encryption?enabled=true" \
-H "Authorization: Bearer $STEADYLINK_ACCESS_TOKEN"{ "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#
/api/platform/ssoworkspace:readReturns { "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#
/api/platform/ssoworkspace:adminBody
issuerstringRequired- The provider's HTTPS issuer URL. SteadyLink reads
/.well-known/openid-configurationfrom 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. enabledbooleanDefaultfalse- Turn the connection on.
enforcebooleanDefaultfalse- 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
}'await steadylink.configureSso({
issuer: "https://northwind.okta.com",
clientId: "0oa8x2k4mQ7",
clientSecret: process.env.OKTA_CLIENT_SECRET,
emailDomains: ["northwind.example"],
enabled: true,
enforce: false,
});{
"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:
Save and enable
PUTthe configuration with"enabled": trueand"enforce": false.Test the login
Open
https://api.steadylink.iofollowed bystartPathin a browser and sign in with an account on one of the email domains. A successful sign-in setstestedAt.Enforce
PUTthe same configuration with"enforce": true. Without a successful test, this returns409with"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.
| Method | Route | Purpose |
|---|---|---|
GET | /api/platform/webhooks | List endpoints and the available event types |
POST | /api/platform/webhooks | Register an endpoint |
POST | /api/platform/webhooks/{webhook_id}/test | Send a test event now |
GET | /api/platform/webhooks/{webhook_id}/deliveries | Recent delivery attempts |
DELETE | /api/platform/webhooks/{webhook_id} | Remove an endpoint |