Verify on your server
Updated
Post the token to https://bot403.com/api/v1/verify with Authorization: Bearer sk_live_YOUR_SECRET_KEY. Send JSON {"solution": "…"}, or post the whole form as application/x-www-form-urlencoded. Bot403 reads the _bot403 field and the honeypot field from the form.
curl -X POST https://bot403.com/api/v1/verify \
-H "Authorization: Bearer sk_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"solution": "<the _bot403 field from the form>"}'
The same call in every language:
// npm install @bot403/node express
import express from "express";
import { Bot403 } from "@bot403/node";
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
const bot403 = new Bot403({ secretKey: process.env.BOT403_SECRET_KEY });
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/contact", async (req, res) => {
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(req.body);
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return res.status(400).send("Try again");
res.send("Thanks.");
});
app.listen(3000);<?php
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
$ch = curl_init("https://bot403.com/api/v1/verify");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("BOT403_SECRET_KEY")],
CURLOPT_POSTFIELDS => http_build_query($_POST),
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
// 2. Refuse when ok is false, except for reason "quota"
if (!$result["ok"] && $result["reason"] !== "quota") {
http_response_code(400);
exit("Try again");
}import os
import requests
from flask import abort, request
# Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
# 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
r = requests.post(
"https://bot403.com/api/v1/verify",
headers={"Authorization": "Bearer " + os.environ["BOT403_SECRET_KEY"]},
data=request.form,
timeout=5,
)
result = r.json()
# 2. Refuse when ok is false, except for reason "quota"
if not result["ok"] and result.get("reason") != "quota":
abort(400)package main
import (
"encoding/json"
"net/http"
"os"
"strings"
)
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
func contact(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
req, _ := http.NewRequestWithContext(r.Context(), "POST", "https://bot403.com/api/v1/verify", strings.NewReader(r.PostForm.Encode()))
req.Header.Set("Authorization", "Bearer "+os.Getenv("BOT403_SECRET_KEY"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
var result struct {
OK bool
Reason string
}
res, err := http.DefaultClient.Do(req)
if err == nil {
defer res.Body.Close()
json.NewDecoder(res.Body).Decode(&result)
}
// 2. Refuse when ok is false, except for reason "quota". On err, let the form through
if err == nil && !result.OK && result.Reason != "quota" {
http.Error(w, "Try again", http.StatusBadRequest)
return
}
w.Write([]byte("Thanks."))
}
func main() {
http.HandleFunc("POST /contact", contact)
http.ListenAndServe(":3000", nil)
}require "net/http"
require "json"
# Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
# 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
uri = URI("https://bot403.com/api/v1/verify")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer " + ENV["BOT403_SECRET_KEY"])
req.set_form_data(request.POST)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
result = JSON.parse(res.body)
# 2. Refuse when ok is false, except for reason "quota"
halt 400, "Try again" if !result["ok"] && result["reason"] != "quota"// npm install @bot403/node
import { Bot403 } from "@bot403/node";
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
const bot403 = new Bot403({ secretKey: process.env.BOT403_SECRET_KEY! });
// Run: bun server.ts
Bun.serve({
port: 3000,
async fetch(req) {
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(await req.formData());
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return new Response("Try again", { status: 400 });
return new Response("Thanks.");
},
});import { Bot403 } from "npm:@bot403/node";
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
const bot403 = new Bot403({ secretKey: Deno.env.get("BOT403_SECRET_KEY")! });
// Run: deno run --allow-net --allow-env server.ts
Deno.serve({ port: 3000 }, async (req) => {
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(await req.formData());
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return new Response("Try again", { status: 400 });
return new Response("Thanks.");
});// npm install @bot403/node hono
import { Hono } from "hono";
import { env } from "hono/adapter";
import { Bot403 } from "@bot403/node";
const app = new Hono();
app.post("/contact", async (c) => {
// Keep the secret key on the server: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
const { BOT403_SECRET_KEY } = env<{ BOT403_SECRET_KEY: string }>(c);
const bot403 = new Bot403({ secretKey: BOT403_SECRET_KEY });
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(await c.req.parseBody());
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return c.text("Try again", 400);
return c.text("Thanks.");
});
export default app; // Bun, Deno and Workers serve this as is; on Node use @hono/node-server// npm install @bot403/node
import { Bot403 } from "@bot403/node";
// The form posts here (action="/contact"). Works on the Node.js and Edge runtimes
export async function POST(request: Request) {
// Keep the secret key on the server, in .env.local: BOT403_SECRET_KEY=sk_live_YOUR_SECRET_KEY
const bot403 = new Bot403({ secretKey: process.env.BOT403_SECRET_KEY! });
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(await request.formData());
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return new Response("Try again", { status: 400 });
return Response.redirect(new URL("/thanks", request.url), 303);
}// npm install @bot403/node
import { Bot403 } from "@bot403/node";
// Keep the secret key on the server: npx wrangler secret put BOT403_SECRET_KEY
// Run: npx wrangler dev
export default {
async fetch(request: Request, env: { BOT403_SECRET_KEY: string }): Promise<Response> {
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
const bot403 = new Bot403({ secretKey: env.BOT403_SECRET_KEY });
// 1. Post the whole form. Bot403 reads the _bot403 field and the honeypot field
const result = await bot403.verifyForm(await request.formData());
// 2. Refuse when ok is false, except for reason "quota"
if (!result.ok && result.reason !== "quota") return new Response("Try again", { status: 400 });
return new Response("Thanks.");
},
};Never put the secret key in a page. The public key is safe to embed; the secret key is not.
Answers and failure reasons
| Answer | Meaning | What to do |
|---|---|---|
{"ok": true, "site": "pk_…", "solved_at": "…"} | A browser solved the puzzle for this site. The token was not used before. | Accept the submission. |
{"ok": false, "reason": "invalid"} | The token is missing, damaged, or the puzzle answer is wrong. | Reject. Log it. |
{"ok": false, "reason": "expired"} | The challenge is older than the site's expiry (default 10 minutes). | Reject. Ask the visitor to send again; the script gets a new token. |
{"ok": false, "reason": "replay"} | The token was used before. | Reject. |
{"ok": false, "reason": "wrong_site"} | The token belongs to a different site. | Reject. Check that the page and the server use keys of the same site. |
{"ok": false, "reason": "honeypot"} | The honeypot field had a value. | Reject, or answer with a fake success. |
{"ok": false, "reason": "quota"}, HTTP 402 | Only in a capped month: after two calendar months over the plan in a row, the next month stops at the limit. A normal month never answers this, however far over you go. | Your choice. The examples above let the submission through. Upgrade in Settings to lift the cap at once. |
Only ok: true counts against the quota. HTTP 401 means the secret key is wrong or missing. HTTP 429 means too many verify calls for the site this minute (100 on Free, 1,000 on Starter, 3,000 on Pro). HTTP 402 (reason quota) only happens in a capped month, above the limit; the token is not burned, so it verifies after an upgrade. Never treat a 402 as a pass. Going over in a normal month changes nothing: verify answers as usual and Bot403 emails you at 80 %, 100 % and 150 %.
When Bot403 is down
The script never blocks a form. If the challenge request fails, the form submits without a token and your verify call answers with reason invalid. If the verify call itself fails with a network error, decide now what your server does: the examples above let the form through.