Keys and your first verdict
Keys
Keys live in Settings → API keys in the dashboard. Your account has live and test keys, and they come in two kinds:
Server — secret keys
Used for the Verdict, feedback, and event APIs. These stay on your server (secrets manager); never expose them in client-side code.
Browser — publishable keys
Used only to mint device tokens. Safe to ship to the browser. See device fingerprinting.
Test mode is fully isolated. Test events, velocity counters, and clusters never affect live data — same engine, separate world.
Install
Node 18.17+. The SDK has zero runtime dependencies; types come from the published API contract. Not on Node? Skip to REST — the API is six endpoints.
First verdict
The same screening, in the two most common shapes:
Next.js route handler
Express middleware
The three decisions both shapes encode:
- Call before you create the account, with the id you are about to use as
external_user_id. That id is how Portreeve links events to accounts, and how a laterreview.resolvedwebhook tells you which user to revoke. - On
block, return a generic error. Don’t tell abusers what tripped them. - On
review(andallow), proceed — and persistresult.idwith the user, guarding onresult.degradedfirst: a degraded verdict has no server event, so itsidisnull(see failure behavior).
When your stack can’t “call first”
The shape above assumes your server mints its own user ids and owns its signup route. Many stacks don’t — here is the honest version for each:
Your auth provider mints the user id
Supabase, Auth0, Clerk, and Firebase all create the id inside their own
sign-up call, so at verdict time there is no external_user_id to send.
Omit it — then attach it right after the provider mints it:
Portreeve keeps the identity mapping. Also persist your own action context
under result.id (the signup and created user in this example) so webhook
enforcement can target the original action. A later
review.resolved webhook names
your user id exactly as if you had sent it up front (and carries the event’s
email as a fallback). The binding is write-once: replaying the same value is
idempotent; a different value is a 409 conflict. Like feedback(),
attachUser() throws on failure — an unbound account would make a later
denial unenforceable, so treat a throw as a bug to fix, not noise.
OAuth and social sign-in
“Sign in with Google” creates the account inside the provider’s redirect —
by the time your callback route runs, the account exists. Screen in the
callback instead: fire the signup verdict there (this path does know the
user id — send it), and on block, disable the just-created account and do
not issue session cookies. On JWT-based auth systems “end its sessions” is
best-effort: banning kills refresh, but already-issued access tokens live
until they expire.
No server signup route at all
If your frontend calls the auth provider directly from the browser, you must first introduce a server signup route — the verdict call needs your secret key and the client’s real IP, and neither belongs in a browser. Budget honestly: that hop touches session cookies, email-confirmation flows, and whatever analytics ride on signup. It is a small rearchitecture, not a drop-in.
Client options and failure behavior
new Portreeve(secretKey, options?):
Fail-open by default
With failMode: "allow" (or "block"), timeouts, network failures, HTTP 5xx,
and invalid responses degrade to your chosen verdict. HTTP 4xx still throws
PortreeveError, including invalid/revoked keys (401) and rate limits (429).
Serialization errors also throw. failMode: "throw" throws on outages too.
A synthesized result is marked:
degraded: true;idandpolicy_versionarenull— there is no server event behind them, so guard onresult.degradedbefore persistingresult.id(both quickstarts above show the pattern);- the reasons carry a
*_failopencode (timeout_failopen,network_failopen, … — see SDK-synthesized codes), so your logs can tell a real verdict from a fail-open one.
A wrong or revoked secret key raises an error; it does not silently allow traffic.
Handle and alert on 401 separately from degraded results. For 429, back off before
retrying with the same action key. Log degraded: true results to monitor outages.
When (and whether) to switch checkout to fail-closed: Handling verdicts.
Safe retries
Use dedupeKey derived from the logical action: a signup email, checkout order ID,
or login attempt ID. Identical serialized bodies replay for 24 hours; changed
context is re-screened under a stable action-and-body key, so retries of that
changed body also replay. All attempts share one timeoutMs budget.
For strict replay, pass idempotencyKey derived from a persisted attempt ID.
Changing its body throws 409; it is not automatically re-screened. Never share
one constant key across unrelated signups. Details under
Idempotency.
Next
What to send for each event type — the canonical payload per event, and the fields whose absence silently switches signals off.