Bolrach Guard API
Make automated abuse expensive. Every attempt costs the caller real work before your server sees it, and the token it produces is single-use and bound to one action.
Quick start
Base URL: https://api.bolrach.com/v1/guard
import { BolrachGuard } from '@bolrach/guard';
const guard = new BolrachGuard({ apiKey: process.env.BOLRACH_GUARD_KEY });
// On your signup route: the page sent a token with the form.
// Verifying SPENDS the token: it is single-use and bound to this action.
const result = await guard.verifyToken({ token: req.body.guard_token, action: 'signup' });
if (!result.valid) {
// reason distinguishes 'expired' (ask again) from 'replayed' (treat as hostile).
return res.status(400).json({ error: 'Could not verify that request.', reason: result.reason });
}
import os
from bolrach_guard import BolrachGuard
guard = BolrachGuard(api_key=os.environ["BOLRACH_GUARD_KEY"])
# Verifying SPENDS the token: it is single-use and bound to this action.
result = guard.verify_token({"token": form["guard_token"], "action": "signup"})
if not result["valid"]:
# reason distinguishes "expired" (ask again) from "replayed" (treat as hostile).
abort(400, "Could not verify that request: " + result["reason"])
Authentication
Verification uses your server key. Assessment uses a public site key, which is safe to ship in a page. Send it as a bearer token:
curl https://api.bolrach.com/v1/guard/... \
-H "authorization: Bearer $YOUR_KEY"
SDKs
Official clients for Node and Python. Both retry on 429 and 5xx with exponential backoff,
obey Retry-After, accept idempotency keys on writes, and raise a typed
BolrachGuardError carrying status, code, message
and requestId, so you branch on the failure instead of parsing a string.
npm install @bolrach/guard
pip install bolrach-guard
Every endpoint below lists its SDK method name. Anything not yet wrapped is still reachable without waiting for a release:
await client.request('GET', '/some/new/endpoint', { query: { days: 7 } }); // node
client.request('GET', '/some/new/endpoint', query={'days': 7}) # python
Errors and retries
Errors are JSON: {"error": {"code": "...", "message": "..."}}. The HTTP status
carries the category, the code carries the specific reason.
| STATUS | CODE | MEANING |
|---|---|---|
400 | bad_request | The body or a query parameter was missing or malformed. The message names the field. |
401 | unauthorized | No key, or a key this API does not recognise. |
403 | forbidden | A valid key without the scope this call needs, or a key not bound to the resource. |
404 | not_found | No such resource, or one that belongs to somebody else. The two are deliberately indistinguishable. |
409 | conflict | The same idempotency key was reused with a different body. |
429 | rate_limited | Too many calls. Retry after the seconds in the Retry-After header, the SDKs already do. |
5xx | server_error | Something failed on our side. Safe to retry; the SDKs retry twice with backoff. |
Idempotency-Key header. Reusing a key returns the original result rather than
applying the change twice, so a timeout you never saw the answer to is safe to repeat.Endpoints
Verify a token your page produced.
Body: token, action. A token is single-use: verifying it spends it.
await client.verifyToken({ /* body */ });
client.verify_token({...})
Ask Guard to judge an attempt. Authenticates with the public site key, not the server key.
Authenticates with the public site key, not the server key, this call is made from a page.
await client.assess({ /* body */ });
client.assess({...})
Mint a proof-of-work challenge directly. The browser SDK does this for you.
await client.challenge({ /* body */ });
client.challenge({...})
Submit a solved challenge and receive a spendable token.
await client.completeChallenge({ /* body */ });
client.complete_challenge({...})