Challenge

# Getting started

Protect a form in three steps. The browser runs an invisible proof-of-work check before the form is sent, and your server verifies the solution.

## 01 Add it to your form

A plain HTML form needs the script and your public key on the form. The browser runs the check when the visitor presses Send, and sends the solution with the form in the `_botpoison` field. A form sent with JavaScript or a framework calls `challenge()` and sends the solution as `_botpoison` itself.

HTML:

HTML

```html
<!-- 1. Import the @botpoison/browser script -->
<script src="https://unpkg.com/@botpoison/browser" async></script>

<!-- 2. Add your public key to the data-botpoison-public-key attribute -->
<form
  action="https://example.demo"
  data-botpoison-public-key="pk_xxxxxxxx"
>
  <input type="text" name="message" />
  <button type="submit">Send</button>
</form>
```

JavaScript:

npm install @botpoison/browser

JavaScript

```javascript
import axios from "axios";

// 1. Import the library
import Botpoison from "@botpoison/browser";

// 2. Create a new instance with your public key
const botpoison = new Botpoison({
    publicKey: 'pk_xxxxxxxx'
});

const sendMessage = async (message) => {
  // 3. Process a challenge
  const { solution } = await botpoison.challenge();
  await axios.post("https://example.demo", {
    message,
    // 4. Forward the solution
    _botpoison: solution,
  });
}
```

React:

npm install @botpoison/browser

JSX

```jsx
import React, { useState } from "react";
import axios from "axios";

// 1. Import the library
import Botpoison from "@botpoison/browser";

// 2. Create a new instance with your public key
const botpoison = new Botpoison({
  publicKey: "pk_xxxxxxxx"
});

const Form = () => {
  const [message, setMessage] = useState("");

  const onSubmit = async (e) => {
    e.preventDefault();
    // 3. Process a challenge
    const { solution } = await botpoison.challenge();
    await axios.post("https://example.demo", {
      message,
      // 4. Forward the solution
      _botpoison: solution,
    });
    alert("Form submitted");
  };

  return (
    <form onSubmit={onSubmit}>
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      <button type="submit">Send</button>
    </form>
  );
};

export default Form;
```

Vue:

npm install @botpoison/browser

Vue

```vue
<template>
  <div>
    <form @submit.prevent="onSubmit">
      <textarea v-model="message" placeholder="Enter your message" />
      <button type="submit" :disabled="loading">Send</button>
    </form>
  </div>
</template>

<script>
import axios from "axios";

// 1. Import the library
import Botpoison from "@botpoison/browser";

export default {
  data() {
    return {
      botpoison: null,
      message: null,
      loading: false,
    };
  },
  created() {
    // 2. Create a new instance with your public key
    this.botpoison = new Botpoison({
      publicKey: "pk_xxxxxxxx",
    });
  },
  methods: {
    async onSubmit() {
      this.loading = true;
      try {
        // 3. Process a challenge
        const { solution } = await this.botpoison.challenge();
        await axios.post("https://example.demo/", {
          message: this.message,
          // 4. Forward the solution
          _botpoison: solution,
        });
        alert("Form submitted");
        this.message = null;
      } catch (error) {
        alert("Error submitting form");
      } finally {
        this.loading = false;
      }
    },
  },
};
</script>
```

Angular:

npm install @botpoison/browser

TypeScript

```typescript
import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";

// 1. Import the library
import Botpoison from "@botpoison/browser";

@Component({
  selector: "app-contact-form",
  template: `
    <form (ngSubmit)="onSubmit()">
      <textarea [(ngModel)]="message" name="message" placeholder="Enter your message"></textarea>
      <button type="submit" [disabled]="loading">Send</button>
    </form>
  `,
})
export class ContactFormComponent {
  message = "";
  loading = false;

  // 2. Create a new instance with your public key
  private botpoison = new Botpoison({
    publicKey: "pk_xxxxxxxx",
  });

  constructor(private http: HttpClient) {}

  async onSubmit() {
    this.loading = true;
    try {
      // 3. Process a challenge
      const { solution } = await this.botpoison.challenge();
      await this.http
        .post("https://example.demo/", {
          message: this.message,
          // 4. Forward the solution
          _botpoison: solution,
        })
        .toPromise();
      alert("Form submitted");
      this.message = "";
    } catch (error) {
      alert("Error submitting form");
    } finally {
      this.loading = false;
    }
  }
}
```

Svelte:

npm install @botpoison/browser

Svelte

```svelte
<script>
    import { onMount } from 'svelte'
    import axios from "axios";

    // 1. Import the library
    import Botpoison from "@botpoison/browser";

    let botpoison = null;
    let message = null;
    let loading = false;

    onMount(() => {
        // 2. Create a new instance with your public key
        botpoison = new Botpoison({
            publicKey: "pk_xxxxxxxx",
        });
    });


    async function onSubmit() {
        loading = true;
        try {
            // 3. Process a challenge
            const { solution } = await botpoison.challenge();
            await axios.post("https://example.demo/", {
                message: message,
                // 4. Forward the solution
                _botpoison: solution,
            });
            alert("Form submitted");
            message = null;
        } catch (error) {
            alert("Error submitting form");
        } finally {
            loading = false;
        }
    }
</script>

<form on:submit|preventDefault="{onSubmit}">
    <textarea bind:value="{message}" />
    <button type="submit" disabled="{loading}">Send</button>
</form>
```

If the check cannot be completed, an HTML form fires `botpoison-challenge-error` and is not sent, and `challenge()` rejects. Tell the visitor what happened. See [events](https://botpoison.com/documentation/libraries/botpoison-browser/#events) in the @botpoison/browser reference.

## 02 Verify it on your server

Before you act on the form, send the solution to `/verify` with your secret key. Keep the secret key on your server, never in the browser.

Node.js:

npm install @botpoison/node

JavaScript

```javascript
// 1. Import the library
import Botpoison from "@botpoison/node";

// or const Botpoison = require("@botpoison/node")

// 2. Create a new instance with your secret key, read from your server's
// environment: BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
const botpoison = new Botpoison({
  secretKey: process.env.BOTPOISON_CHALLENGE_SECRET_KEY
});

server.post("/message", async (req, res) => {
  const { message, _botpoison } = req.body;
  // 3. Verify the solution
  const { ok } = await botpoison.verify(_botpoison);
  if (!ok) {
    throw new Error("Invalid Botpoison solution");
  }
  // ...
});
```

PHP:

PHP

```php
<?php
// The secret key comes from your server's environment:
// BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
$secret_key = getenv("BOTPOISON_CHALLENGE_SECRET_KEY");

// 1. Send the solution from the form's _botpoison field to /verify
$response = file_get_contents("https://api.botpoison.com/verify", false, stream_context_create(["http" => [
    "method" => "POST",
    "header" => "Content-Type: application/json\r\nAuthorization: Bearer " . $secret_key,
    "content" => json_encode(["solution" => $_POST["_botpoison"]]),
    "ignore_errors" => true,
]]));

// 2. Accept the form only when ok is true
if (!json_decode($response)->ok) {
    http_response_code(403);
    exit;
}
// ...
```

Python:

Python

```python
import os
import requests
from flask import Flask, abort, request

# The secret key comes from your server's environment:
# BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
SECRET_KEY = os.environ["BOTPOISON_CHALLENGE_SECRET_KEY"]

app = Flask(__name__)

@app.post("/message")
def message():
    # 1. Send the solution from the form's _botpoison field to /verify
    result = requests.post(
        "https://api.botpoison.com/verify",
        headers={"Authorization": f"Bearer {SECRET_KEY}"},
        json={"solution": request.form["_botpoison"]},
        timeout=8,
    ).json()
    # 2. Accept the form only when ok is true
    if not result.get("ok"):
        abort(403)
    # ...
```

Go:

Go

```go
package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
)

// The secret key comes from your server's environment:
// BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
var secretKey = os.Getenv("BOTPOISON_CHALLENGE_SECRET_KEY")

func message(w http.ResponseWriter, r *http.Request) {
	// 1. Send the solution from the form's _botpoison field to /verify
	body, _ := json.Marshal(map[string]string{"solution": r.FormValue("_botpoison")})
	req, _ := http.NewRequest("POST", "https://api.botpoison.com/verify", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+secretKey)
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		http.Error(w, "", http.StatusBadGateway)
		return
	}
	defer res.Body.Close()
	// 2. Accept the form only when ok is true
	var result struct{ Ok bool `json:"ok"` }
	json.NewDecoder(res.Body).Decode(&result)
	if !result.Ok {
		http.Error(w, "", http.StatusForbidden)
		return
	}
	// ...
}
```

Rust:

Rust

```rust
use axum::{extract::Form, http::StatusCode};
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize)]
struct Verification {
    ok: bool,
}

// The secret key comes from your server's environment:
// BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
async fn message(Form(form): Form<HashMap<String, String>>) -> StatusCode {
    let secret_key = std::env::var("BOTPOISON_CHALLENGE_SECRET_KEY").unwrap_or_default();
    // 1. Send the solution from the form's _botpoison field to /verify
    let solution = form.get("_botpoison").cloned().unwrap_or_default();
    let response = reqwest::Client::new()
        .post("https://api.botpoison.com/verify")
        .bearer_auth(secret_key)
        .json(&HashMap::from([("solution", solution)]))
        .send()
        .await;
    let Ok(response) = response else {
        return StatusCode::BAD_GATEWAY;
    };
    // 2. Accept the form only when ok is true
    match response.json::<Verification>().await {
        Ok(Verification { ok: true }) => {}
        _ => return StatusCode::FORBIDDEN,
    }
    // ...
    StatusCode::OK
}
```

Java:

Java

```java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class MessageController {
    // The secret key comes from your server's environment:
    // BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxx
    private static final String SECRET_KEY = System.getenv("BOTPOISON_CHALLENGE_SECRET_KEY");

    private final HttpClient client = HttpClient.newHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();

    @PostMapping("/message")
    public void message(@RequestParam(name = "_botpoison", defaultValue = "") String solution) throws Exception {
        // 1. Send the solution from the form's _botpoison field to /verify
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.botpoison.com/verify"))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + SECRET_KEY)
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(Map.of("solution", solution))))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        // 2. Accept the form only when ok is true
        if (!mapper.readTree(response.body()).path("ok").asBoolean()) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN);
        }
        // ...
    }
}
```

HTTP:

Shell

```bash
curl --request POST \
  --url 'https://api.botpoison.com/verify' \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --header 'authorization: Bearer sk_xxxxxxxx' \
  --data '{"solution": "xxxxxxxx"}'
```

The [@botpoison/node reference](https://botpoison.com/documentation/libraries/botpoison-node/) covers `verify()` in full, and the [REST API](https://botpoison.com/documentation/challenge/rest-api/) covers the request from any other language.

## 03 Read the result

JSON

```json
// The status is 200 for every result. Read "ok" to decide whether to accept
// the submission. When "ok" is false, "message" says why.

// Accepted
{ "ok": true }

// Rejected, with the reason
{ "ok": false, "message": "Invalid solution" }
```

Accept the form when `ok` is `true`. Otherwise, reject it. `message` says why.

Test key

Verify with the secret key `test` to try both paths without a browser. The solution `ok` passes and any other solution fails. Test calls never count toward your plan. See [Challenge test mode](https://botpoison.com/documentation/challenge/test-mode/).
