Challenge
Getting started
Read the Botpoison Challenge docs page "Getting started" at https://botpoison.com/documentation/challenge/getting-started.md and use it to make this change in my project. Use the stack this project already runs on, and ask me for my keys rather than inventing them.
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.
01Add 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.
<!-- 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>import axios from "axios";// 1. Import the libraryimport Botpoison from "@botpoison/browser";// 2. Create a new instance with your public keyconst 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, });}import React, { useState } from "react";import axios from "axios";// 1. Import the libraryimport Botpoison from "@botpoison/browser";// 2. Create a new instance with your public keyconst 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;<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 libraryimport 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>import { Component } from "@angular/core";import { HttpClient } from "@angular/common/http";// 1. Import the libraryimport 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; } }}<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 in the @botpoison/browser reference.
02Verify 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.
// 1. Import the libraryimport 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_xxxxxxxxconst 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// 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 trueif (!json_decode($response)->ok) { http_response_code(403); exit;}// ...import osimport requestsfrom flask import Flask, abort, request# The secret key comes from your server's environment:# BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxxSECRET_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) # ...package mainimport ( "bytes" "encoding/json" "net/http" "os")// The secret key comes from your server's environment:// BOTPOISON_CHALLENGE_SECRET_KEY=sk_xxxxxxxxvar 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 } // ...}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_xxxxxxxxasync 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}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;@RestControllerpublic 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); } // ... }}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 covers verify() in full, and the REST API covers the request from any other language.
03Read the result
// 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.
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.