JavaScript packages

Updated

Three small packages on npm, typed, with no dependencies. @bot403/node verifies tokens on your server, @bot403/browser gets tokens in the browser, and @bot403/react wraps that in a hook. Each one is a thin layer over the same two requests, so the curl call below is exactly what the server package sends:

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>"}'
npm install @bot403/node       # your server
npm install @bot403/browser    # fetch() submissions, forms you build in JavaScript
npm install @bot403/react      # the useBot403 hook

On the server: @bot403/node

It runs wherever fetch exists: Node.js 18 and later, Bun, Deno, Cloudflare Workers, Vercel and Next.js route handlers. Keep the secret key in an environment variable.

// npm install @bot403/node
import { Bot403 } from "@bot403/node";

// Next.js App Router: app/api/contact/route.js
export async function POST(request) {
  const bot403 = new Bot403({ secretKey: process.env.BOT403_SECRET_KEY });
  const result = await bot403.verifyForm(await request.formData());
  if (!result.ok && result.reason !== "quota") {
    // result.reason: expired, replay, invalid, wrong_site, honeypot or automation
    return Response.json({ error: "Spam check failed" }, { status: 400 });
  }
  // ...handle the form
  return Response.json({ ok: true });
}

verifyForm sends every field of the form, so a honeypot field set on the site is checked too. verify(token) checks one token when you already have it. Both resolve with ok: true, or with ok: false and a reason. The verify page explains each reason.

They reject with a Bot403Error only when the request itself fails: a wrong secret key (status 401), too many verify calls (429), or no network (status 0). Decide what your form does in that case; letting it through is the usual answer, because Bot403 being unreachable is not a reason to lose a message.

quota comes with HTTP 402 and only in a capped month (after two calendar months over the plan). The package returns it as a normal result. The examples in these docs let the form through on quota; block it instead if spam costs you more than a lost message.

import { Bot403 } from "@bot403/node";

// Options: a self-hosted instance and a shorter timeout
const bot403 = new Bot403({
  secretKey: process.env.BOT403_SECRET_KEY,
  baseUrl: "http://localhost:8087",
  timeoutMs: 5000,
});

// Totals for the plan window and the quota: used, limit, cap
const stats = await bot403.stats();

In the browser: @bot403/browser

A plain HTML form needs only the script tag. Use this package when you submit with fetch() or build forms in JavaScript. It adds the same 6 KB script to the page once and gives it types.

// npm install @bot403/browser
import { solve, attach, FIELD } from "@bot403/browser";

// A fresh single-use token for one request
const token = await solve("pk_live_YOUR_PUBLIC_KEY");
await fetch("/contact", { method: "POST", body: new URLSearchParams({ email: "[email protected]", [FIELD]: token }) });

// Or protect a form element you created
const form = document.createElement("form");
await attach(form, { publicKey: "pk_live_YOUR_PUBLIC_KEY" });

load({ publicKey }) adds the script with the key, so it protects every form on the page, as the script tag does. Pass nonce if your Content Security Policy uses nonces; the script options page lists the CSP lines.

React: @bot403/react

// npm install @bot403/react
import { useBot403, FIELD } from "@bot403/react";

export function ContactForm() {
  const getToken = useBot403("pk_live_YOUR_PUBLIC_KEY");
  async function onSubmit(event) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    body.set(FIELD, await getToken());
    await fetch("/api/contact", { method: "POST", body });
  }
  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <button>Send</button>
    </form>
  );
}

The hook starts loading the script when the component mounts, so the first token is quick. Call getToken() once per submission: tokens are single-use.

MIT license. Packages: @bot403/node, @bot403/browser, @bot403/react.