Embeddable forms
Drop a workflow into any website.
Publish any workflow as a <wrk-form> web component.
Paste one script tag and one element into any page - your marketing
site, a partner portal, a customer app - and collect structured,
AI-ready submissions without an iframe.
How it works
In the admin UI, open any published workflow and switch on embedding. The platform mints a per-workflow embed token and gives you a two-line snippet to paste wherever you want the form to render.
- No iframe -
<wrk-form>is a native web component that renders in an open shadow DOM, so it stays visually isolated from the host page's CSS but remains scriptable from it. - No login required - respondents submit anonymously, identified only by a short-lived respondent token.
- Same backend - submissions land in the same
workflow_instancesand event stream as every other entry point, so your AI hooks, webhooks, and SSE consumers all fire as usual. - Tenant-safe - every embed endpoint is scoped to the token's workflow; rotating or disabling the token takes effect immediately.
Quickstart
Paste this into any HTML page. Replace the token and base URL with the values shown in the admin embed panel.
<script src="https://wrk.ing/wrk-form.js" defer></script>
<wrk-form
token="a1b2c3d4e5f6..."
base-url="https://api.wrk.ing"
></wrk-form> That's it. The script registers the custom element; the element fetches the workflow structure, walks the respondent through each step, and completes the submission on the final step.
Treat the token like a public API key
The embed token is safe to ship in client-side HTML, but it grants anyone who holds it the ability to start submissions for that workflow. Pair it with an origin allowlist (below) to restrict which sites can use it, and rotate the token from the admin UI if it leaks.
Attributes
| Attribute | Required | Description |
|---|---|---|
token | Yes | Per-workflow embed token minted in the admin UI. |
base-url | No | API origin, e.g. https://api.wrk.ing. Defaults to the origin the script itself was loaded from. |
theme | No | light, dark, or auto (default). auto follows the host page's prefers-color-scheme. |
All three attributes are observed - change them at runtime and the component re-renders.
Events
The component dispatches CustomEvents on itself. Events
bubble and are composed: true, so they cross the shadow DOM
boundary and you can listen on the element directly or on any ancestor.
| Event | Fires when | Detail |
|---|---|---|
wrk-form:started | The respondent begins a new submission. | { response_id: number } |
wrk-form:step | A step is successfully submitted and the form advances. | { step_id: number, index: number } |
wrk-form:completed | The final step is submitted and the response is marked complete. | { response_id: number } |
wrk-form:error | Structure load, submission, or upload fails. | { message: string } |
Listen for events to drive analytics, conditional redirects, or custom confirmation UI:
const form = document.querySelector("wrk-form");
form.addEventListener("wrk-form:started", (e) => {
console.log("submission started", e.detail.response_id);
});
form.addEventListener("wrk-form:completed", (e) => {
window.location.href = "/thanks?id=" + e.detail.response_id;
});
form.addEventListener("wrk-form:error", (e) => {
console.error("embed error", e.detail.message);
});
Origin allowlist
Each workflow carries an optional embed_allowed_origins
list. When set, the API rejects submissions from any other origin with a
403. When empty, the token itself is the only credential,
and any origin that loads the script can submit.
Manage allowed origins in the admin embed panel or via the workflows API:
curl -X PATCH https://api.wrk.ing/api/workflows/42 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"embed_allowed_origins": [
"https://www.example.com",
"https://partners.example.com"
]
}' Origins are matched by exact string, including scheme and port. The check runs server-side on every write, so it cannot be bypassed by a crafted CORS preflight.
The respondent token flow
Submissions are multi-step. The web component handles this for you, but if you drive the endpoints directly (see headless usage below) the order matters:
GET /api/public/embed/:token/structure- returns the workflow, its steps, and all elements, validated and normalised for rendering.POST /api/public/embed/:token/start- creates a newworkflow_instanceand returns{ response_id, respondent_token }.POST /api/public/embed/:token/steps/:stepId- upserts answers for one step. Send therespondent_tokenin theX-Wrking-Respondent-Tokenheader.POST /api/public/embed/:token/uploads- optional; attach files to in-progress answers.POST /api/public/embed/:token/complete- finalises the response and emits theworkflow_instance.completedevent.
The respondent token is generated server-side on start and
scoped to that single response. It is short-lived and cannot be reused
across submissions. Do not store it client-side beyond the active form
session.
File uploads
Elements of type file, avatar, and
document are uploaded via a dedicated endpoint so the main
step submission stays JSON. The web component handles this
automatically - a <input type="file"> change fires a
multipart upload and the resulting upload_id is stored as
the element's value.
| Constraint | Value |
|---|---|
| Max file size | 10 MB |
| Files per request | 1 |
| Allowed MIME types | image/jpeg, image/png,
image/webp, image/gif,
application/pdf, text/plain,
text/csv, application/msword,
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
application/vnd.ms-excel,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
Uploads are stored using the organization's configured storage driver (local, S3, or equivalent) and served through signed URLs.
Rate limits
Embed endpoints are rate-limited per token and per client IP, using a rolling hourly window. These limits protect your workflow from bots and runaway scripts without getting in the way of legitimate traffic.
| Operation | Limit | Window |
|---|---|---|
POST /start | 30 requests | 1 hour |
POST /steps/:stepId | 300 requests | 1 hour |
POST /uploads | 20 requests | 1 hour |
When the limit is exceeded the API returns 429. Back off and retry after a brief delay.
Styling & theming
The component renders in an open shadow DOM, so host-page CSS does not
leak in by default. Every visual primitive is driven by a CSS custom
property on :host, which you can override from the outside
to match your brand.
<wrk-form
token="..."
base-url="https://api.wrk.ing"
style="
--wf-primary: #c2703a;
--wf-primary-hover: #a85e2f;
--wf-radius: 12px;
--wf-font: 'Inter', system-ui, sans-serif;
"
></wrk-form> Useful custom properties:
| Property | Default (light) | Purpose |
|---|---|---|
--wf-bg | #ffffff | Surface background |
--wf-fg | #0f172a | Primary text colour |
--wf-muted | #64748b | Secondary text colour |
--wf-border | #e2e8f0 | Input and container borders |
--wf-primary | #4f46e5 | Primary action / focus ring |
--wf-primary-hover | #4338ca | Primary action hover |
--wf-danger | #b91c1c | Error text and borders |
--wf-radius | 8px | Surface corner radius |
--wf-font | system-ui stack | Font family |
With theme="auto" (the default) the component follows the
host page's prefers-color-scheme automatically, mapping each
light variable to a dark equivalent. Set theme="light" or
theme="dark" to force a mode.
Headless usage
The web component is a convenience wrapper around five REST endpoints. Any language that can speak HTTP and JSON can drive them directly - a native mobile app, a server-side proxy, a Zapier action, a one-off script.
| Method | Path | Description |
|---|---|---|
GET | /api/public/embed/:token/structure | Fetch the workflow shape |
POST | /api/public/embed/:token/start | Begin a submission, receive a respondent token |
POST | /api/public/embed/:token/steps/:stepId | Submit answers for one step |
POST | /api/public/embed/:token/uploads | Upload a file for the current response |
POST | /api/public/embed/:token/complete | Finalise the response |
Full schemas and Try it out buttons for each endpoint live in the interactive API reference under the Embed tag.
curl https://api.wrk.ing/api/public/embed/$TOKEN/structure const BASE = "https://api.wrk.ing";
const TOKEN = "a1b2c3d4e5f6...";
const structure = await fetch(`${BASE}/api/public/embed/${TOKEN}/structure`)
.then(r => r.json());
const start = await fetch(`${BASE}/api/public/embed/${TOKEN}/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
}).then(r => r.json());
const { response_id, respondent_token } = start;
for (const section of structure.sections) {
await fetch(`${BASE}/api/public/embed/${TOKEN}/steps/${section.step_id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wrking-Respondent-Token": respondent_token,
},
body: JSON.stringify({
response_id,
answers: section.elements.map(el => ({ element_id: el.id, value: answersFor(el) })),
}),
});
}
await fetch(`${BASE}/api/public/embed/${TOKEN}/complete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wrking-Respondent-Token": respondent_token,
},
body: JSON.stringify({ response_id }),
}); Downstream side effects
Embed submissions are first-class citizens. Every start, step, and completion emits a structured event on the platform event bus and is visible to:
- AI prompt hooks attached to the workflow and its steps
- Webhook endpoints subscribed to
workflow_instance.*events (see Webhooks) - SSE consumers (see Events & SSE)
- MCP resources and tools (see MCP server)
The submission appears in the admin UI alongside any entries captured
through the in-platform workflow runner, with submitted_via
set to embed and the originating HTTP Origin
captured on the audit record.