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_instances and 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

AttributeRequiredDescription
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.

EventFires whenDetail
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:

  1. GET /api/public/embed/:token/structure - returns the workflow, its steps, and all elements, validated and normalised for rendering.
  2. POST /api/public/embed/:token/start - creates a new workflow_instance and returns { response_id, respondent_token }.
  3. POST /api/public/embed/:token/steps/:stepId - upserts answers for one step. Send the respondent_token in the X-Wrking-Respondent-Token header.
  4. POST /api/public/embed/:token/uploads - optional; attach files to in-progress answers.
  5. POST /api/public/embed/:token/complete - finalises the response and emits the workflow_instance.completed event.

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.

ConstraintValue
Max file size10 MB
Files per request1
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.

OperationLimitWindow
POST /start30 requests1 hour
POST /steps/:stepId300 requests1 hour
POST /uploads20 requests1 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:

PropertyDefault (light)Purpose
--wf-bg#ffffffSurface background
--wf-fg#0f172aPrimary text colour
--wf-muted#64748bSecondary text colour
--wf-border#e2e8f0Input and container borders
--wf-primary#4f46e5Primary action / focus ring
--wf-primary-hover#4338caPrimary action hover
--wf-danger#b91c1cError text and borders
--wf-radius8pxSurface corner radius
--wf-fontsystem-ui stackFont 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.

MethodPathDescription
GET/api/public/embed/:token/structureFetch the workflow shape
POST/api/public/embed/:token/startBegin a submission, receive a respondent token
POST/api/public/embed/:token/steps/:stepIdSubmit answers for one step
POST/api/public/embed/:token/uploadsUpload a file for the current response
POST/api/public/embed/:token/completeFinalise 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.