A clear trust boundary.
An inspectable result.
The browser observes. Your backend requests an assessment. Your application decides what to do. Later, your team reports what it confirmed.
The action lifecycle
- Your backend prepares a form using
POST /v1/attempts. It supplies a configured action and origin, a server-derived session binding and a unique form ID. - The browser optionally sends a minimal snapshot to
POST /v1/collectand receives an opaque receipt. - The ordinary form POST reaches your backend. Your application validates its own fields, session, CSRF token and rate controls.
- Your backend calls
POST /v1/assess. The API checks binding and integrity, applies versioned rules and persists an assessment. - In shadow mode, the action continues. Operators inspect the evidence in the console.
- When your team later establishes what happened, your backend reports it with
POST /v1/outcomes: the outcome, how it was established and what you did about it. The report is stored beside the assessment and never changes it.
Two integration modes
Your server code chooses the mode; a visitor cannot. Bound mode, the default, costs a server call each time the form is rendered and in return ties each submission to that render: the action, the session, one use, an expiry and the snapshot collected for it. Submit-only mode calls Blokk only when the form is submitted. It suits cached pages, but gives up that binding and the collection history, so its browser evidence is weaker and can be replayed. It must be enabled per action in the site configuration, and its results say integrity: unbound. Missing browser evidence is never treated as an integration failure or a bot signal in either mode.
The Node integration
Northstar Studio, the separate local test website, uses the same public SDK methods in bound mode. The API secret stays on the server.
import { BlokkClient } from '@blokk/server-sdk';
const blokk = new BlokkClient({
apiUrl: process.env.API_URL,
apiKey: process.env.BLOKK_API_KEY,
siteId: process.env.BLOKK_SITE_ID,
});
// When rendering the form. Server-generated values; never trust hidden fields for these.
const attempt = await blokk.prepareForm({
action: 'contact_submit',
expectedOrigin: configuredOrigin,
sessionBinding,
submissionId: formId,
});
// After your own form validation and CSRF checks:
const assessment = await blokk.assess({
attemptId: attempt.attemptId,
receipt: optionalBrowserReceipt,
action: 'contact_submit',
sessionBinding,
idempotencyKey: formId,
context: validatedRequestContext,
policyMode: 'shadow',
});In submit-only mode there is no attempt to prepare:
import { parseRelayedObservations } from '@blokk/server-sdk';
// The action must be enabled for submit-only mode in the site configuration.
const assessment = await blokk.assess({
integrationMode: 'submit_only',
...parseRelayedObservations(form.blokk_observations),
action: 'newsletter_signup',
sessionBinding,
idempotencyKey: submissionId,
context: validatedRequestContext,
});Later, once your team has established what the action was:
await blokk.reportOutcome({
assessmentId: assessment.assessmentId,
outcome: 'confirmed_unwanted', // or 'legitimate'; 'unresolved' is not ground truth
basis: 'manual_review', // how it was established, from a fixed list
actionTaken: 'content_removed', // what you did; defaults to 'none'
determinedAt: reviewedAt,
idempotencyKey: reviewId,
});These excerpts show the contract. The complete runnable example, browser collection, request-context derivation and failure handling are in apps/test-site. Do not copy placeholders into a production integration.
Six separate questions
| Axis | What it tells you |
|---|---|
| Automation | Indicated, suspicious, no indicators observed, or indeterminate. |
| Evidence availability | Whether usable observations were available for the rules. |
| Integrity | Whether the attempt and receipt were valid, missing, invalid, expired, replayed or mismatched, with a reason for anything other than valid. |
| Integration mode | Bound or submit-only, as your server chose. Submit-only results are unbound by design. |
| Policy and action | What your website chose to do: shadow mode does not block, and any action you take later, such as suspending an account, is your policy. |
| Confirmed outcome | What your team later established—confirmed unwanted, legitimate or unresolved—reported separately. Only the first two are ground truth. An automated action can be legitimate; a block or a suspension is not a confirmed outcome. |
What leaves the browser
By default the optional SDK reports only its version and the browser’s automation flag. Interaction telemetry is off: it attaches no pointer, keyboard, touch or scroll listeners and reads no timing. Only when your server configures a named telemetry experiment does it also report which kinds of interaction occurred and a bucketed elapsed time, and no rule uses them. It never collects form contents, raw keystrokes, pointer trails, fingerprints or persistent device identifiers.
All browser fields are assertions. A valid receipt only confirms receipt of a bounded observation; it does not establish that JavaScript ran honestly, or that a person is human.
Accounts
For actions inside a signed-in account, your server can send an account reference: a keyed hash of the account ID, derived on your server from the account you authenticated, with your own secret. Blokk hashes it again and counts that account’s actions of one type on one site. Ten or more within a minute are at most suspicious, because batch use can look the same. The reference is pseudonymous, not anonymous. It links one account’s activity and cannot show that one person runs several accounts.
Failures and retries
Attempts expire after five minutes. The demo can refresh its attempt without discarding the form. Exact retries reuse the stored assessment; altered input under the same key is rejected. A consumed attempt cannot become reusable by changing the idempotency key.
A bad or missing receipt is an integrity result, not an error. An invalid request is a 4xx error to fix in your integration. Only a timeout, a network failure or a 5xx means Blokk is unavailable; the server SDK labels that kind of error unavailable. Then the default shadow integration continues under the website’s own controls and records that no assessment was available. Your application owns its business operation and must also make that operation idempotent.
Current limits
These are narrow, explicit rules. Low-rate or sophisticated automation may pass. No-pointer use, autofill, keyboard-only interaction and missing JavaScript are not standalone automation verdicts. Scripted tests do not establish a real-world false-positive rate.
AI agents are assessed through the same general indicators as other automation. There are no agent-specific rules, and Blokk does not identify which agent acted or verify who it acts for. An agent acting for a real customer may be welcome; that is a decision for your policy.
Detailed contracts, threat model, run reports, retention operations and setup instructions are included with the source. Read the draft privacy notice and draft alpha and pilot terms.