> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heretic.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Plain HTML

> Run a complete widget form with a Node backend in about ten minutes.

This example serves a form at `/check` and saves one receipt after successful server verification. It includes the backend, browser-session binding, error handling, and a persistent record that prevents duplicate writes.

## 1. Run locally

Install Node 24 or later. [Download and extract the server examples](https://heretic.tech/downloads/heretic-server-examples.zip), then run:

```sh theme={"dark"}
cd server-examples
cp .env.example .env
npm start
```

Open `http://127.0.0.1:3000/check`. Wait for the widget, then choose **Save receipt**. Repeating the same submission returns the same receipt.

The supplied configuration uses public test keys and labels the page as local test mode. These keys exercise the form and API connection; they do not exercise browser measurements, phone ceremonies, or production policy checks. Production mode rejects test proofs.

## 2. Use your keys

[Create a site and verify its domain](/integration/challenge-widget#before-you-start). In your server's private environment, set:

```dotenv theme={"dark"}
APP_ORIGIN=https://shop.example
HERETIC_SITE_KEY=hrtc_live_your_public_site_key
HERETIC_SECRET_KEY=hrtc_sk_your_server_secret
HERETIC_SITE_ID=site_your_site_id
HERETIC_ALLOW_TEST=false
HERETIC_DATABASE=/persistent-data/heretic-example.sqlite
```

Use the exact dashboard values in place of the labels above. `APP_ORIGIN` is your form's exact HTTPS origin, with no path or trailing slash. Restart the server. The local HTTP server listens on loopback; serve it through your existing HTTPS reverse proxy.

## 3. The form

The example generates the attempt ID on the server and saves it with an HttpOnly browser-session cookie. Its HTML includes:

```html theme={"dark"}
<form action="/check" method="post">
  <input type="hidden" name="attempt" value="SERVER_ATTEMPT_ID">
  <div class="heretic-guard"
    data-sitekey="YOUR_PUBLIC_SITE_KEY"
    data-action="receipt"
    data-cdata="SERVER_ATTEMPT_ID"></div>
  <button type="submit">Save receipt</button>
</form>
<script src="https://heretic.tech/guard.js" async defer></script>
```

The downloadable server replaces those placeholders. The widget adds the hidden `heretic-response` field. Its default mode requests a phone challenge after a contradicted or refused probe.

The POST handler checks the request Origin and saved session, then calls siteverify with server-chosen policy, hostname, action, attempt ID and site ID. Missing proof, failed verification and network errors cannot save a receipt. See [verification policies](/api/siteverify-endpoint#request) before changing the example's `uncontradicted` policy.

## 4. Connect your write

`examples/lib/form-flow.mjs` saves the receipt and consumes the attempt in one SQLite transaction. Replace that write with your application's account or order write in the same transaction. Bind authenticated account IDs and submitted business data to the saved attempt as well.

For serverless or multiple-host deployments, use your application's shared database with equivalent transactions and unique constraints. A local SQLite file needs persistent storage. The standalone API clients work independently of this example store.

The example's CSP permits `https://heretic.tech` in `script-src` and `frame-src`. Merge those sources with your existing policy. Its response includes `Cache-Control: no-store` so caches cannot share a visitor's pending form.

## Check the integration

1. Submit without a token: the write is refused.
2. Repeat a successful submission: the same receipt returns.
3. Change the attempt or use another browser session: the write is refused.
4. Interrupt verification: the action stays pending; retry the same submission.
5. Use real keys to check an uncontradicted probe and a phone challenge on your verified domain.

Test keys accept repeated tokens and bypass production context checks. Real keys are required for the final step.


## Related topics

- [Node, Python and PHP](/integration/server-clients.md)
- [Challenge widget](/integration/challenge-widget.md)
- [Quickstart](/quickstart.md)
- [SvelteKit](/integration/sveltekit.md)
- [Next.js](/integration/nextjs.md)
