Skip to content

Widgets and collections API

Manage origin-restricted upload widgets and their submissions, and publish ordered collections as branded file portals.

On this page

This page covers two ways to share with people outside your workspace. Upload widgets let visitors to your own website send you files, restricted to the origins you list. Collections gather files into an ordered set and publish it as a portal at https://steadylink.io/c/{slug}. Both follow the stable-link model: a widget can replace a file in place, and a collection item can follow the current revision or stay pinned to one.

Access model#

  • Management routes accept a signed-in session or an API key. With a key, GET routes need assets:read and every other method needs assets:write. That includes portal analytics, which needs assets:read, not analytics:read.
  • Public routes under /api/public/ need no credentials. Widgets are protected by the browser Origin and a one-time session token; portals by their access mode.
  • Plan limits. Active widgets: 1 on Hobby, 3 on Personal, 15 on Pro, 100 on Business. Collections: 3, 15, 100, and 500. Going over returns 409 with "code": "feature_limit_reached".

Upload widgets#

A widget is a configuration plus a public key (wdg_...) that you embed in your site. In create mode each upload becomes a new file in a bucket. In replace mode each upload becomes a new revision of one file, so its stable link serves the new bytes. With "approvalMode": "review", nothing is published until someone approves it.

Create a widget#

POST/api/widgets
Requiresassets:write

Body

namestringRequired
Internal name, up to 200 characters.
allowedOriginsstring[]Required
From 1 to 30 origins allowed to use the widget, as scheme and host only: https://www.northwind.example. A value can also hold several origins separated by commas or spaces. Paths, queries, and credentials are rejected.
modestringDefault create
create or replace.
destinationBucketIduuid
Required in create mode. Bucket for new files.
assetIduuid
Required in replace mode. The file that uploads replace.
collectionIduuid
Add each published file to this collection.
acceptedTypesstring[]
Up to 30 rules: MIME types (application/pdf), wildcards (image/*), or extensions (.pdf). Empty accepts any type.
maxBytesintegerDefault 26214400
Per-file limit, up to 2 GB and no more than the plan's upload limit (413 otherwise).
buttonTextstringDefault Choose a file
Up to 80 characters.
themestringDefault light
light, dark, or auto.
approvalModestringDefault review
review holds uploads for approval. auto publishes them immediately.
allowAutoPublishbooleanDefault false
Must be true when approvalMode is auto. It is a deliberate acknowledgment that anyone on an allowed origin can publish.
activebooleanDefault true
Inactive widgets refuse uploads and do not count toward the plan limit.
curl
curl -X POST https://api.steadylink.io/api/widgets \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Campaign intake",
    "mode": "create",
    "destinationBucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
    "acceptedTypes": ["image/*", ".pdf"],
    "maxBytes": 26214400,
    "allowedOrigins": ["https://www.northwind.example", "https://staging.northwind.example"],
    "approvalMode": "review"
  }'
201 CreatedResponse
{
  "id": "2e7a9c40-5b1d-4f83-a6e2-9c0d8b3f1e57",
  "name": "Campaign intake",
  "publicKey": "wdg_8Hk2...",
  "mode": "create",
  "destinationBucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10",
  "assetId": null,
  "collectionId": null,
  "acceptedTypes": ["image/*", ".pdf"],
  "maxBytes": 26214400,
  "buttonText": "Choose a file",
  "theme": "light",
  "allowedOrigins": ["https://www.northwind.example", "https://staging.northwind.example"],
  "approvalMode": "review",
  "active": true,
  "createdAt": "2026-10-08T12:00:00.000000",
  "updatedAt": "2026-10-08T12:00:00.000000"
}

Validation errors, such as auto without allowAutoPublish or create without a bucket, return 422. To embed the widget, see the upload widget guide.

List widgets#

GET/api/widgets
Requiresassets:read

Returns every widget in the workspace, newest first, including publicKey. The public key is not a secret; the origin list is what restricts it.

Update a widget#

PATCH/api/widgets/{widget_id}
Requiresassets:write

Replaces the whole configuration. Despite the method, send every field as you would for create; omitted fields go back to their defaults. The public key does not change. Reactivating a widget counts toward the plan limit.

Delete a widget#

DELETE/api/widgets/{widget_id}
Requiresassets:write

Deletes the widget and any uploads still waiting for approval. Returns 204 No Content. Pages that embed it stop working.

List widget submissions#

GET/api/widget-submissions
Requiresassets:read

Returns the 200 most recent uploads that reached a decision point, newest first.

200 OKResponse
{
  "items": [
    {
      "id": "d6c1f093-8e2a-4b75-9f40-3a7e5b2d1c86",
      "widgetId": "2e7a9c40-5b1d-4f83-a6e2-9c0d8b3f1e57",
      "widgetName": "Campaign intake",
      "mode": "create",
      "filename": "storefront.jpg",
      "contentType": "image/jpeg",
      "byteSize": 1820443,
      "origin": "https://www.northwind.example",
      "status": "awaiting_approval",
      "resultAssetId": null,
      "createdAt": "2026-10-08T12:41:09.000000",
      "expiresAt": "2026-10-15T12:41:20.000000"
    }
  ]
}

status is awaiting_approval, approved, rejected, failed, or expired. A submission waits seven days for a decision, then can no longer be approved.

Download a widget submission#

GET/api/widget-submissions/{submission_id}/download
Requiresassets:read

Returns url (download) and previewUrl (inline), both valid for 5 minutes, plus filename and contentType. Only works while the submission is awaiting_approval; otherwise 404.

Approve a widget submission#

POST/api/widget-submissions/{submission_id}/approve
Requiresassets:write

In create mode, commits the upload as a new file in the widget-submissions/ folder of the destination bucket. In replace mode, adds it as a new revision of the target file. If the widget has a collectionId, the file is appended to that collection.

200 OKResponse
{ "status": "approved", "assetId": "7c3e9f10-2b5d-4a86-9e1f-0d4c8b2a6e53" }

Errors: 409 if it is not awaiting approval or the destination no longer exists, 410 once it has expired, 422 if the file fails validation. A failed approval leaves the submission failed.

Reject a widget submission#

POST/api/widget-submissions/{submission_id}/reject
Requiresassets:write

Deletes the uploaded bytes and returns { "status": "rejected" }.

Public: initialize an upload#

POST/api/public/widgets/{public_key}/initialize
No API key. Public route.

Called from the browser on an allowed origin. SteadyLink checks the Origin header, the file's type and size, and the workspace's temporary storage, then returns a presigned upload URL valid for ten minutes.

Body

filenamestringRequired
Up to 512 characters.
sizeintegerRequired
Exact size in bytes.
contentTypestringDefault application/octet-stream
The file's MIME type, checked against acceptedTypes.
200 OKResponse
{
  "sessionToken": "Wc0q...",
  "uploadUrl": "<presigned PUT URL>",
  "expiresAt": "2026-10-08T12:51:09.000000",
  "permission": "create"
}

PUT the bytes to uploadUrl with Content-Type: application/octet-stream. Errors: 403 for an inactive widget or an origin not on the list, 413 above the smaller of the widget's maxBytes and the plan's upload limit, 415 for a type that is not accepted.

Public: complete an upload#

POST/api/public/widgets/{public_key}/complete
Widget session token

Send the sessionToken as Authorization: Bearer <sessionToken> from the same origin that initialized the upload. The token works once.

  • With review, returns { "status": "awaiting_approval", "assetId": null, "replacementRequestId": null } and notifies the workspace.
  • With auto, publishes immediately and returns { "status": "published", "assetId": "...", "replacementRequestId": null }. New files go to the root of the destination bucket.

Errors: 409 if the session expired or was already used, 403 if the origin differs, 400 if the uploaded size does not match, 422 if the file fails validation.

CORS preflight requests to /api/public/widgets/ are answered for any origin, but every actual request is checked against allowedOrigins, and the response allows only the validated origin.

Collections#

A collection is an ordered list of files with an access mode. Each item either follows the file's current revision or is pinned to one retained revision. Items whose revision has not passed the malware scan are left out of the portal.

accessModeWho can open the portal
privateNobody. Public routes return 404. Use it for drafts.
publicAnyone with the link.
passwordAnyone who enters the password.
expiringAnyone with the link until expiresAt.

Any collection with an expiresAt in the past returns 410 from the public routes, whatever its mode.

The collection object#

Collection
{
  "id": "c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48",
  "name": "Spring press kit",
  "description": "Logos, product shots, and the fact sheet.",
  "slug": "spring-press-kit-4f9a1c",
  "accessMode": "public",
  "expiresAt": null,
  "noindex": true,
  "viewCount": 312,
  "downloadCount": 87,
  "itemCount": 12,
  "createdAt": "2026-09-30T09:00:00.000000",
  "updatedAt": "2026-10-08T12:10:00.000000",
  "canCustomizeBranding": true,
  "branding": {
    "name": "Northwind",
    "logoUrl": "https://www.northwind.example/logo.svg",
    "url": "https://www.northwind.example",
    "primaryColor": "#0b0e0c",
    "accentColor": "#b8ff5a",
    "removeSteadyLinkBranding": false
  }
}

The portal URL is https://steadylink.io/c/ followed by slug. The JavaScript SDK adds it to every collection as url. itemCount is null in the response to an update.

List collections#

GET/api/collections
Requiresassets:read
curl https://api.steadylink.io/api/collections \
  -H "X-API-Key: $STEADYLINK_API_KEY"

Returns { "items": [...] } with every collection, most recently updated first.

Create a collection#

POST/api/collections
Requiresassets:write

Body

namestringRequired
Up to 200 characters. Also used to build the slug, with a random suffix.
descriptionstring
Up to 2000 characters.
accessModestringDefault private
private, public, password, or expiring.
passwordstring
8 to 128 characters. Required for password. Stored only as a hash.
expiresAtdatetime
Required and in the future for expiring. Optional for other modes.
noindexbooleanDefault true
Ask search engines not to index the portal.
brandNamestring
Portal branding. Branding fields require Pro, Business, or Enterprise; otherwise the request fails with 403.
brandLogoUrlstring
HTTPS URL of a logo.
brandUrlstring
HTTPS link for the brand name.
primaryColorstring
Six-digit hex, such as #0b0e0c.
accentColorstring
Six-digit hex.
removeSteadyLinkBrandingbooleanDefault false
Hide SteadyLink branding on the portal.
curl -X POST https://api.steadylink.io/api/collections \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Spring press kit", "accessMode": "password", "password": "tulip-harbor-91" }'

Returns the collection object with status 201 Created.

Get a collection#

GET/api/collections/{collection_id}
Requiresassets:read
curl https://api.steadylink.io/api/collections/c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48 \
  -H "X-API-Key: $STEADYLINK_API_KEY"

Returns the collection object plus its items in portal order:

200 OKResponse
{
  "id": "c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48",
  "name": "Spring press kit",
  "...": "other collection fields",
  "items": [
    {
      "id": "61f0b8d3-a2c7-4e95-8b14-7d3e0a9c2f56",
      "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
      "name": "logo-primary.svg",
      "position": 0,
      "version": 3,
      "pinned": false,
      "mime": "image/svg+xml",
      "byteSize": 18244,
      "width": null,
      "height": null,
      "createdAt": "2026-09-30T09:02:00.000000",
      "previewUrl": "/api/public/collections/spring-press-kit-4f9a1c/items/61f0b8d3-a2c7-4e95-8b14-7d3e0a9c2f56/preview"
    }
  ]
}

version is the revision the portal serves now: the pinned one, or the current one when pinned is false.

Update a collection#

PATCH/api/collections/{collection_id}
Requiresassets:write

Send only the fields to change; they are the same as for create. Switching to password requires a password unless one is already set, and switching to expiring requires an expiresAt unless one is already set (422 otherwise). Send "expiresAt": null to remove an expiration.

curl -X PATCH https://api.steadylink.io/api/collections/c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48 \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accessMode": "public" }'

Delete a collection#

DELETE/api/collections/{collection_id}
Requiresassets:write

Deletes the collection and its items, not the files. Returns 204 No Content. The portal link stops working.

Add an item#

POST/api/collections/{collection_id}/items
Requiresassets:write

Body

assetIduuidRequired
A file in the workspace.
versioninteger
Pin this revision. Omit to follow the current revision, so replacements show up in the portal automatically.
curl -X POST https://api.steadylink.io/api/collections/c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48/items \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44", "version": 12 }'
201 CreatedResponse
{ "id": "61f0b8d3-a2c7-4e95-8b14-7d3e0a9c2f56", "assetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44", "position": 4, "pinnedVersion": 12 }

New items go to the end. Errors: 409 if the file is already in the collection, 422 if the file or revision does not exist.

Reorder items#

PUT/api/collections/{collection_id}/items/order
Requiresassets:write

Send itemIds with every item ID in the collection exactly once, in the new order. A missing, extra, or repeated ID returns 422, which protects you from overwriting a change someone else just made. Returns { "updated": true }.

curl -X PUT https://api.steadylink.io/api/collections/c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48/items/order \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "itemIds": ["61f0b8d3-a2c7-4e95-8b14-7d3e0a9c2f56", "0b9d4e7a-5c21-4f38-a6e0-2d8b1f7c3e94"] }'

Pin or unpin an item#

PATCH/api/collections/{collection_id}/items/{item_id}
Requiresassets:write

Send { "version": 12 } to pin a revision, or { "version": null } to follow the current revision again. Returns { "updated": true, "pinnedVersion": 12 }, or 422 if the revision does not exist.

curl -X PATCH https://api.steadylink.io/api/collections/c8a4e1f6-3b72-4d09-95e3-0f1b7d2a6c48/items/61f0b8d3-a2c7-4e95-8b14-7d3e0a9c2f56 \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "version": null }'

Remove an item#

DELETE/api/collections/{collection_id}/items/{item_id}
Requiresassets:write

Removes the item from the collection, not the file. Returns 204 No Content. In JavaScript: steadylink.removeFromCollection(collectionId, itemId).

Get portal analytics#

GET/api/collections/{collection_id}/analytics
Requiresassets:read

Returns total views and downloads plus up to the last 90 days that had activity, oldest first.

200 OKResponse
{
  "viewCount": 312,
  "downloadCount": 87,
  "days": [
    { "day": "2026-10-07", "views": 41, "downloads": 9 },
    { "day": "2026-10-08", "views": 18, "downloads": 3 }
  ]
}

A view is counted each time the portal data is loaded; a download each time an item is downloaded. Previews are not counted as downloads.

Public: exchange a password for an access token#

POST/api/public/collections/{slug}/unlock
No API key. Public route.

Send { "password": "tulip-harbor-91" }. Returns { "accessToken": "...", "expiresIn": 3600 }, a token valid for one hour or until the collection expires, whichever is sooner. Pass it as the access query parameter on the other public routes. A wrong password returns 401.

Public: read a portal#

GET/api/public/collections/{slug}
No API key. Public route.

Query parameters

accessstring
The token from the password exchange. Required for password collections.
countbooleanDefault true
Set false to load the portal without counting a view, for example when re-rendering.

Returns the collection fields, the resolved branding, and items as in Get a collection. The resolved branding includes showSteadyLinkBranding, and falls back to the workspace name and default colors when no custom branding applies. Errors: 404 for an unknown or private collection, 410 once expired, 401 with "code": "collection_password_required" when a password collection is opened without a valid token.

Public: preview an item#

GET/api/public/collections/{slug}/items/{item_id}/preview
No API key. Public route.

Redirects (307) to a short-lived URL for inline display, or streams the bytes for encrypted files. Previews count toward the workspace's delivery usage but not toward portal downloads.

Public: download an item#

GET/api/public/collections/{slug}/items/{item_id}/download
No API key. Public route.

Redirects (307) to a short-lived URL that downloads the file under its name, or streams it for encrypted files. Counts a portal download and delivery usage. Returns 409 if the revision has not passed scanning.

Next steps#