# Fugte Widget Build Reference > The complete format for writing a Fugte widget. If you are an AI > assistant generating widget code, this file is the specification to follow. Source of truth: https://fugte.com/docs/build/ Editor: https://fugte.app/ Last updated: 2026-07-26 A Fugte widget is a small, embeddable web app. You write it as one .html file and the editor splits it into four. The editor previews it live, and the same source is rendered by Fugte when the widget is published, so a widget that previews correctly will almost always publish correctly. This page is the build reference. It is written to be read by people and by AI assistants: if you are asking ChatGPT, Claude, Gemini, or a coding agent to write a Fugte widget, point it here first, or paste the primer below into the conversation. You do not need any of this to use Fugte. Text, colors, images, and options are editable in the Customize tab without touching code. This is for the people (and models) writing the code underneath. ## Prompt primer for AI assistants Copy this into your AI before asking it for a widget. It is the short version of everything on this page. ```text You are writing a Fugte widget. Follow these rules exactly. Output ONE .html file. Do not split it up. It has four regions, in this order, and the editor separates them for you on paste: All styles. Liquid works here. markup Liquid + HTML only. NO doctype, html, head, or body tags. This region is a fragment. Vanilla JS, no imports, no build step. Liquid works here. {% schema %}...{% endschema %} Settings JSON that generates the Customize form. Every region is optional except the markup, but a widget with no schema has nothing to customize, which is the point of the product. A ` can close the script block early, so prefer `textContent` rendering and keep the seed to fields you control the shape of. #### Optimistic local echo To let a visitor see their own entry the moment they submit, watch the success element and render the captured values yourself. This is per-browser and cosmetic: everyone else still sees rows only through `data.*`. ```js var pending = null; var form = document.querySelector("form"); form.addEventListener("submit", function () { pending = { message: this.elements.message.value, name: this.elements.name.value }; }); var ok = document.querySelector(".webdyi-form-success"); new MutationObserver(function () { if (ok.style.display !== "none" && pending) { render(pending); pending = null; form.reset(); } }).observe(ok, { attributes: true, attributeFilter: ["style", "class"] }); ``` `localStorage` works inside the sandboxed iframe (Fugte bridges it), so past entries by the same visitor can be restored on the next visit. The platform's form SDK still handles the real submission. ### `{% payment %}`: accept money Use `{% payment %}` for donations, tips, or pay-what-you-want flows. Do **not** include Stripe.js, a payment link, or your own checkout. Money goes to the owner's own connected Stripe account. ```liquid {% payment "tip" %} {{ payment.amount }} {{ payment.status }} {% endpayment %} ``` The tag renders a `
` wrapper, and the payment SDK is attached automatically. | Variable | Output | | --- | --- | | `{{ payment.amount }}` | Amount input (``) | | `{{ payment.status }}` | Status and error message container | | `{{ payment.cause }}` | The cause text from the payments config | | `{{ payment.min }}` | Minimum amount, whole currency units | | `{{ payment.max }}` | Maximum amount, whole currency units | | `{{ payment.currency }}` | Currency code, for example `USD` | | `{{ payment.symbol }}` | Currency symbol, for example `$` | The config values are there so the block can describe itself to the buyer. ```liquid {% payment "tip" %}

{{ payment.cause }}

Give {{ payment.symbol }}{{ payment.min }} to {{ payment.symbol }}{{ payment.max }} {{ payment.currency }}

{{ payment.amount }} {{ payment.status }} {% endpayment %} ``` Printing `min` and `max` is presentation only. Both are still enforced server-side at checkout. For a fixed price, omit `{{ payment.amount }}` and use a preset button. ```liquid {% payment "tip" %} {{ payment.status }} {% endpayment %} ``` Add a top-level `payments` array in the schema. The `id` must match the `{% payment %}` name. ```json { "payments": [ { "id": "tip", "currency": "usd", "min": 1, "max": 500, "cause": "Support my work" } ] } ``` `min` and `max` are in whole currency units. `cause` is shown to the buyer on the Stripe checkout page. ### Secure remote sources: live data from authenticated APIs Widgets can pull live data from external APIs server-side, so an API key or bearer token never ships to the browser. Declare a top-level `sources` array in the schema and render the fetched data via the Liquid `sources` variable. ```json { "sources": [ { "id": "weather", "url": "https://api.example.com/current?city=Paris", "refresh": 300 } ] } ``` ```liquid {% if sources.weather %}

{{ sources.weather.temp }}°C, {{ sources.weather.summary }}

{% endif %} ``` How it works: - Fugte fetches each source URL **server-side** at render time and caches the body for `refresh` seconds (clamped 60 to 86400, default 300). A thousand visitors cost one upstream call, and if the upstream errors, the last cached body is served. - **Secrets**: the owner pastes the API key in the editor (Iframe tab, Data sources, after publishing). It is stored per widget and source, then attached as a request header (default `Authorization`, configurable, for example `X-Api-Key`). Never put a key in markup, script, or schema: everything in the widget files is client-visible. - Responses are parsed by `format`: `json` (the default when the body parses), `csv` (an array of rows keyed by the header row), `xml`, or `text`. Set `"format": "csv"` explicitly for spreadsheet exports. - Kinds for common inputs: `"kind": "rss"` (a feed, or a site to discover one on), `"youtube"` (a channel, handle, or playlist), `"google-sheet"` (a shared sheet, read as CSV rows), `"shopify-products"` (a store). `rss` and `youtube` give `{ title, link, description, items: [{ title, link, date, summary, image, author }] }`. - **A spreadsheet the owner uploads**: expose a `csv_picker` setting and bind it as a CSV source with `"url": "{{ settings. }}"`. The owner drops a `.csv` (or pastes a Google Sheet link (General access: Anyone with the link)) in Customize; the widget reads `sources.` as rows keyed by the header. See Spreadsheets below. - Limits and safety: https URLs only, max 3 sources, 8 second timeout, 256KB body cap. Localhost, internal, and private-IP targets are rejected. - `sources.*` is **empty in the editor preview**, like `data.*`, with one exception: a `csv` source bound to a `csv_picker` setting shows its rows in the preview as soon as the owner uploads a file. Always guard with `{% if sources.x %}` and render sensible placeholder content without it. - For public CORS APIs that need no auth, a plain client-side `fetch` in `script.js` is fine. Use `sources` when auth, caching, or CORS is the problem. ### `{% video %}`: embed a video A `video_picker` setting stores either an uploaded file URL or a YouTube/Vimeo link. Render it with `{% video %}`, never a raw `