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#
- Open Share > Upload widgets and choose New widget.
- Give it a Name your team will recognize in the review queue, such as
Website contact form. - 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.
- Choose the accepted file types and the Max size (MB).
- List the Websites allowed to embed, one per line, for example
https://example.com. - Leave Review each upload before it goes live on unless you fully trust everyone who can reach the page.
- 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:
<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-textstringDefaultChoose 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-themestringDefaultlightdarkrenders 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:
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:
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
statusstringawaiting_approvalwhen the widget reviews uploads, orpublishedwhen the file went live immediately.assetIdstring | null- The new or replaced file's asset ID when
statusispublished.nullwhile 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:
- The browser asks
POST /api/public/widgets/{public_key}/initializefor a session, sending the file's name, size, and type. SteadyLink checks that the request'sOriginis 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. - 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.
- The browser PUTs the bytes directly to that URL with
Content-Type: application/octet-stream. - The browser calls
POST /api/public/widgets/{public_key}/completewith the session token asAuthorization: Bearer. The request must come from the same origin that started the session, and the stored file must be exactly the declared size. - 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#
| Limit | Value |
|---|---|
| Session and upload URL | Ten minutes from initialization. |
| File size | The widget's Max size, which cannot exceed your plan's maximum upload size or 2 GB. |
| Accepted types | Up to 30 entries: MIME types, wildcards such as image/*, or extensions such as .pdf. An empty list accepts any type. |
| Allowed origins | 1 to 30. |
| Files per upload | One. Visitors can upload again after each file finishes. |
| Review window | Seven days. |
Troubleshooting#
| What the visitor sees | Cause |
|---|---|
| Nothing renders | The 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.