SendFlo

Upload widget

Put one of your SendFlo upload pages on your own site: inline, as a popup, or as a button that opens one. Uploaders send files without leaving your page, and your page hears what happens. The script widget is part of Pro, Team Pro and Team Enterprise. The plain iframe (Receive → your link → Embed) works on every plan.

Quick start

Add the script once, then mark where each upload page goes. Use the exact version with its integrity hash:

<script src="https://sendflo.app/widget/1.1.0/widget.js"
        integrity="sha384-13mAeskKGS0sNx0qNnysknaSq7hs/dB8IcP7rlA3LfyxtxTcQvLn7mhc6MNHABwY"
        crossorigin="anonymous" defer></script>

<div data-sendflo-upload="acme-kyc"></div>

Replace acme-kyc with your upload page's address (sendflo.app/u/acme-kyc). Your site must be one of the page's allowed sites: set them in the page's settings under Script widget, or with embed_origins in the API. A page with no allowed sites can be shown on any site.

For automatic updates within version 1, load /widget/v1.js instead (no integrity hash, since its content changes).

Inline, popup or button

<!-- inline (default): the page sits in your layout and grows to fit -->
<div data-sendflo-upload="acme-kyc"></div>

<!-- a button that opens the page in a popup -->
<div data-sendflo-upload="acme-kyc" data-mode="button" data-label="Upload your documents"></div>

<!-- prefill: the uploader can still change these -->
<div data-sendflo-upload="acme-kyc" data-name="Ana Ruiz" data-email="ana@example.com"
     data-answer-ticket_number="4821"></div>

Or from JavaScript:

const widget = SendFlo.mount(document.getElementById("upload"), {
  page: "acme-kyc",
  mode: "inline",            // or "popup" / "button"
  email: user.email,
  answers: { ticket_number: "4821" },
  onEvent: (type, data) => console.log(type, data),
});

// A popup without a button:
const popup = SendFlo.open("acme-kyc", { email: user.email });
popup.close();

Prefilled values are sent to the page in a message after it loads, never in its address, so they don't end up in logs.

Events

Every event reaches onEvent, widget.on(type, fn), and a DOM event named sendflo:<type> on the element. Messages go only to your page's exact origin.

ready            { page, version }
resize           { height }                       // the inline frame resizes itself
verified         { email, via: "code" | "link" | "site" }
upload_started   { upload_id, files: [{ name, size }] }
progress         { upload_id, file, fraction, index, count }
upload_complete  { upload_id, item_id, file }
all_complete     { upload_id, files: [{ name, size, item_id }], answers? }
error            { code, message }
close            {}                               // the popup was closed
fullscreen       { on }                           // the document scanner needs the whole screen

all_complete carries the uploader's answers only when the page's Share answers with your site setting is on. It is off by default. item_id is the received file's id in the API (GET /v1/inbox-items).

Verified by your site

If your site already knows who the uploader is, your server can say so. The page then treats them as verified, with no emailed code, and you see the files as "verified by" your site. Turn it on by issuing a widget secret in the page's settings (or POST /v1/file-requests/:id/widget-secret). Keep the secret on your server.

// On your server (Node, Bun, Deno)
import { signUploaderToken } from "@sendflo/upload-widget/server";
const token = signUploaderToken(process.env.SENDFLO_WIDGET_SECRET!, {
  page: "acme-kyc", email: user.email, name: user.name,   // ttlSeconds: up to 600
});

// In the page
SendFlo.mount(el, { page: "acme-kyc", token });
// or: token: () => fetch("/api/sendflo-token").then((r) => r.text())

In another language: the token is base64url(JSON) + "." + base64url(HMAC-SHA256(secret, the first part)), with the claims v: 1, page, email, name (optional), exp (Unix seconds, at most 10 minutes ahead) and nonce (16–64 letters, digits, - or _). Each token works once, for that page only.

import base64, hashlib, hmac, json, secrets, time
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
claims = {"v": 1, "page": "acme-kyc", "email": email, "exp": int(time.time()) + 300, "nonce": secrets.token_urlsafe(18)}
payload = b64(json.dumps(claims).encode())
token = payload + "." + b64(hmac.new(secret.encode(), payload.encode(), hashlib.sha256).digest())

React

npm install @sendflo/upload-widget
import { SendFloUpload } from "@sendflo/upload-widget/react";

<SendFloUpload page="acme-kyc" email={user.email} onEvent={(type, data) => track(type, data)} />

The same package exports mount, open and requestUpload for other frameworks.

A secure intake from your chat

When your bot asks someone in a conversation for a file, it opens a secure intake and gives your chat page the intake's client token. SendFlo.requestUpload({ token }) opens it in the popup. The token works once, so the transcript never keeps a working upload link. The bot never sees the files. See Secure intakes (/docs/intakes).

SendFlo.requestUpload({ token: intake.client_token, onEvent: (type, data) => { /* all_complete, close, error … */ } });

Sites with a Content Security Policy

Allow SendFlo to load the script and the frame:

script-src https://sendflo.app; frame-src https://sendflo.app

Inside the frame the page uses no cookies: everything it needs stays in memory. When the uploader verifies by email, the button in the email opens a new tab; they pick the number the widget shows, and the widget is verified too.