Skip to content

Upload widget

Accept files from visitors on your own website with an origin-restricted embed, review them before they go live, and react to completed uploads in JavaScript.

On this page

The upload widget is a drop-in upload box for your website. Use it when people outside your workspace need to send you files, such as artwork from customers, documents from applicants, or an updated logo from a partner, and you do not want to build an upload backend. You configure it in the dashboard, paste two lines of HTML into your site, and uploads land in a bucket you choose or replace one specific file. By default nothing goes live until someone in your workspace approves it.

Create a widget#

  1. Open Share > Upload widgets and choose New widget.
  2. Give it a Name your team will recognize in the review queue, such as Website contact form.
  3. Under Uploads go to, choose:
    • New files in a bucket to collect new files, then pick the Bucket.
    • Replace one file to let visitors send a new version of one existing file, then pick the File to replace. Approving an upload publishes it as the next revision behind that file's existing link.
  4. Choose the accepted file types and the Max size (MB).
  5. List the Websites allowed to embed, one per line, for example https://example.com.
  6. Leave Review each upload before it goes live on unless you fully trust everyone who can reach the page.
  7. Save, then choose Copy embed code on the widget's card.

The number of active widgets depends on your plan (one on Free). A paused widget does not count; use Pause widget to stop accepting uploads without deleting the configuration.

Embed it#

Paste the embed code where the upload box should appear. It looks like this:

HTML
<div
  data-steadylink-widget="wdg_8Zk3pQ..."
  data-api="https://api.steadylink.io"
  data-button-text="Send artwork"
  data-accept="image/*,.pdf"
  data-max-bytes="26214400"
  data-theme="light"
></div>
<script async src="https://steadylink.io/widget/v1.js"></script>

The script finds every element with data-steadylink-widget when the page loads and renders an upload box inside it (in a shadow root, so your site's CSS does not leak in). The box has a button, a drop zone, a progress bar, and a status line announced to screen readers.

Embed attributes

data-steadylink-widgetstringRequired
The widget's public key, starting with wdg_. It identifies the configuration and is safe to publish. It is not a workspace credential and grants nothing on its own.
data-apiURLRequired
The SteadyLink API origin, https://api.steadylink.io. Without it the element stays empty.
data-button-textstringDefault Choose a file
Label of the upload button.
data-acceptstring
Comma-separated MIME types, wildcards such as image/*, or extensions such as .pdf. Filters the file picker and gives an instant error for other types.
data-max-bytesinteger
Largest file in bytes. Larger files get an instant error instead of a failed upload.
data-themestringDefault light
dark renders the dark style. Any other value renders the light style.
data-callbackstring
Name of a global function to call with the result when an upload completes, for example onArtworkUploaded.

data-accept and data-max-bytes only improve the visitor's experience. The rules that matter are the ones saved on the widget, and SteadyLink enforces them on its side for every upload. Editing the attributes in a browser does not bypass them.

Pages that render later#

If your page adds the element after load (a modal, a client-side route, a React component), mount it yourself once the script has loaded:

JavaScript
window.SteadyLinkUpload.mount(document.querySelector("#artwork-upload"));
// or re-scan the whole page
window.SteadyLinkUpload.scan();

Mounting the same element twice is ignored, so calling scan() after every route change is safe.

React to completed uploads#

When an upload finishes, the element dispatches a steadylink:complete event that bubbles, and calls the data-callback function if you set one. The event's detail is the completion result:

JavaScript
document.querySelector("[data-steadylink-widget]").addEventListener("steadylink:complete", (event) => {
  const { status, assetId } = event.detail;
  if (status === "awaiting_approval") {
    showMessage("Thanks. We will review your file shortly.");
  } else {
    showMessage(`Received. File ID ${assetId}`);
  }
});

document.querySelector("[data-steadylink-widget]").addEventListener("steadylink:error", (event) => {
  console.warn("Upload failed:", event.detail.message);
});

steadylink:complete detail

statusstring
awaiting_approval when the widget reviews uploads, or published when the file went live immediately.
assetIdstring | null
The new or replaced file's asset ID when status is published. null while the upload waits for review.
replacementRequestIdstring | null
Reserved. Currently always null.

The widget also updates its own status line ("Submitted for review" or "Upload complete"). steadylink:error fires when validation, the transfer, or completion fails, with detail.message describing the failure.

Security and upload flow#

The widget never sees a workspace API key or storage credentials. Each upload works like this:

  1. The browser asks POST /api/public/widgets/{public_key}/initialize for a session, sending the file's name, size, and type. SteadyLink checks that the request's Origin is on the widget's list, that the widget is active, and that the file fits the widget's type and size rules and your plan's limits.
  2. It answers with a ten-minute, origin-bound session with one create or replace permission: a one-time session token and a presigned upload URL that both expire after ten minutes.
  3. The browser PUTs the bytes directly to that URL with Content-Type: application/octet-stream.
  4. The browser calls POST /api/public/widgets/{public_key}/complete with the session token as Authorization: Bearer. The request must come from the same origin that started the session, and the stored file must be exactly the declared size.
  5. The session is consumed at that moment and cannot be replayed. The upload either waits for review or is published.

Origins are matched on scheme, host, and port only. https://example.com, https://www.example.com, and http://example.com are three different origins, so list every variant your site is served from. A widget accepts up to 30 origins. Paths are not allowed in an origin, and pages on any path of a listed origin can use the widget.

Because the public key is visible in your page's HTML, anyone can copy the embed code. The origin check stops other websites from using it in a browser, but it cannot stop a determined person sending requests from a script with a forged Origin header. That is the reason review is on by default.

Review uploads before they go live#

With Review each upload before it goes live on, completed uploads stay in temporary storage. They do not appear in your buckets and do not change any live file until a workspace member approves them. Your workspace is notified with "Widget upload needs approval".

Open Share and go to the Review queue to preview each upload, download it, and choose Approve or Reject:

  • Approve on a New files widget adds the file to the widget's bucket, in a folder called widget-submissions/, where it is finalized and scanned like any upload. On a Replace one file widget it publishes the upload as the next revision of the target file, so the file's existing link starts serving it.
  • Reject deletes the temporary file. This cannot be undone.

Uploads waiting for review expire after seven days and can no longer be approved. Deleting a widget also deletes any uploads still waiting for review. Approvals and rejections are also available through the Sharing API.

Publishing without review#

Turning review off publishes each upload as soon as it completes: new files go to the root of the widget's bucket, and replacements go live behind the target file's link. The dashboard warns you that "Files sent through this widget will go live as soon as they finish uploading."

Uploaded files are scanned for malware either way, and a file that fails the scan is never served.

Limits#

LimitValue
Session and upload URLTen minutes from initialization.
File sizeThe widget's Max size, which cannot exceed your plan's maximum upload size or 2 GB.
Accepted typesUp to 30 entries: MIME types, wildcards such as image/*, or extensions such as .pdf. An empty list accepts any type.
Allowed origins1 to 30.
Files per uploadOne. Visitors can upload again after each file finishes.
Review windowSeven days.

Troubleshooting#

What the visitor seesCause
Nothing rendersThe script did not load, data-api is missing, or the element was added after load without calling mount().
"The upload could not be initialized."The page's origin is not on the widget's list, the widget is paused or deleted, or the file is too large or the wrong type for the widget's saved rules.
"The file could not be uploaded."The transfer to storage failed or took longer than ten minutes. Try again.
"The upload could not be completed."The session expired, was already used, or the file did not pass validation.

Open Share to check the widget's origins and rules, then load your page from exactly one of the listed origins. You can try a widget on the example page once its origin list includes https://steadylink.io.

Next steps#