---
type: tutorial
title: Add the widget to your site
summary: Add the widget to your React contact form, confirm a real verification in your dashboard, then enforce it on your Node backend.
---

You have BananaSeed's public key from the [previous lesson](/docs/getting-started/create-your-account). Now we put the widget on the React contact form, watch the dashboard confirm a real visitor cleared it, and then add the one server call that turns that into real protection.

## What you will have at the end

- The widget on your contact form, verifying visitors in the browser.
- Your dashboard showing those verifications.
- Your Node backend dropping any submission that has no valid token.

## 1. Add the widget to your React form

Install the package. It registers the `<caputchin-widget>` element (and the `<caputchin-game>` element for the next lesson):

```bash
npm install @caputchin/widget
```

Import it once at startup, such as in your app entry (`main.jsx`):

```js
import "@caputchin/widget";
```

Now drop the widget into BananaSeed's contact form. When the checkbox clears, the widget adds a hidden `caputchin-token` field to the form, so you read it on submit exactly like your own fields. Replace `cpt_pub_...` with your public key:

```jsx
export function ContactForm() {
  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    await fetch("/api/contact", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        email: data.get("email"),
        message: data.get("message"),
        token: data.get("caputchin-token"),
      }),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <caputchin-widget sitekey="cpt_pub_..." />
      <button type="submit">Send</button>
    </form>
  );
}
```

On a plain HTML page with no build step, or inside a native mobile app? See [the script-tag (CDN) integration](/docs/integration-guides/cdn) and [the mobile app guide](/docs/integration-guides/mobile-apps).

## 2. See it verify

Run your app and open the contact form. The Caputchin checkbox appears and completes on its own, with no click required. You do not even need to submit the form yet: that is already a real verification, and it has reached Caputchin.

## 3. Confirm it in your dashboard

Back in the dashboard, open your troop and click into your site key. Its statistics show a funnel: **Sessions started**, **Client-completed**, **Server-verified**, and **Threats blocked**. Refresh the page; **Sessions started** and **Client-completed** both now read at least `1`. That is your visit: the widget loaded (started) and the challenge was solved in the browser (client-completed), with no backend involved at all.

**Server-verified** is still `0`, because nothing on your server has checked a token yet. That gap is the next step.

<Callout type="success">Your widget is live, and the dashboard is already logging real verifications, before you have written a line of backend code. 🎉</Callout>

## 4. Verify the token on your backend

Client-completed means the visitor cleared the challenge, but your form is not protected until your server confirms the token. A bot can ignore the widget entirely and POST straight to your endpoint. So your backend checks every token with Caputchin before trusting the request. Your secret (`cpt_sec_...`) lives in an environment variable, never in the browser:

```js
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/contact", async (req, res) => {
  const { email, message, token } = req.body;

  const verdict = await fetch("https://verify.caputchin.com/v1/siteverify", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      secret: process.env.CAPUTCHIN_SECRET,
      response: token,
    }),
  }).then((r) => r.json());

  if (!verdict.success) {
    return res.status(400).json({ error: "Could not verify you are human." });
  }

  // Verified. Handle the real message: store it, email it, whatever you do.
  res.json({ ok: true });
});

app.listen(3001);
```

That `verdict.success` check is the point: a bot posting straight to `/api/contact` carries no token, `success` comes back `false`, and the message is dropped before you ever touch it.

No backend? No problem. Turn on [hosted verification](/docs/hosted-verification/overview) and Caputchin runs this check for you, forwarding only verified submissions to a webhook or your inbox.

## 5. Confirm the server check

Submit the contact form once more, now that your endpoint is verifying. Go back to your site key's statistics and refresh: **Server-verified** now reads at least `1` too, sitting alongside Started and Client-completed. The whole funnel is accounted for, browser to server.

To read that funnel the other way (what the gaps between the counts tell you about visitor drop-off and integration mistakes), see [statistics and sessions](/docs/site-keys/statistics).

<Callout type="success">Your contact form is genuinely protected now: every submission is checked on your server, and tokenless bots are turned away. 🎉</Callout>

## What just happened

| Step | Where | Result |
|---|---|---|
| `<caputchin-widget>` in your form | React | The checkbox verifies on mount and adds a one-time `caputchin-token` field. |
| Open the page | Browser | Started and Client-completed tick, no backend needed. |
| `/siteverify` with your secret | Node to Caputchin | `success: true` lets the request through; no token means it is dropped, and Server-verified climbs. |

## Common questions

### Do I need a backend to use the widget?

For the browser step, no. For real protection, yes. The widget verifies in the browser on its own, but a bot can ignore it and POST straight to your endpoint, so your server must confirm the `caputchin-token` before trusting a submission. If you do not run a backend, turn on [hosted verification](/docs/hosted-verification/overview) and Caputchin runs that check for you.

### Why is Server-verified still zero after the widget loads?

Because nothing on your server has checked a token yet. Sessions started and Client-completed tick from the browser alone; Server-verified only climbs once your backend calls `/siteverify`, which is [step 4](#4-verify-the-token-on-your-backend).

### Does the widget work outside React?

Yes. It is a standard custom element, so it drops into plain HTML, Vue, Svelte, Angular, or a native app WebView with the same one tag. See [client side integration](/docs/integration-guides/cdn) and [the mobile app guide](/docs/integration-guides/mobile-apps).

### Where does my secret key go?

Only on your server, in an environment variable (`cpt_sec_...`). It authenticates the `/siteverify` call and must never be shipped to the browser.

## Next

The checkbox works. Now the fun part: swap it for a real playable game, with no change to your backend.

Continue to [Add a game from the marketplace](/docs/getting-started/add-a-game).
