Documentation

GrayPass is the behavioral identity layer. Send the SDK, drop in the middleware, and verify users by how they behave instead of what they type. This page covers everything you need to integrate.

Quickstart

Five minutes from npm install to a verified session.

1. Install

npm install @graypass/sdk @graypass/middleware

2. Initialize on the client

import { GrayPassV2 } from "@graypass/sdk";

// init returns a client instance - keep it for the session lifetime
const gp = GrayPassV2.init({ apiKey: "pk_test_…" });

const { sessionId } = await gp.startSession({ userId: "u_42" });

gp.on("trust_change", ({ trust, state }) => {
  console.log("trust", trust, state);
});

// Gate a sensitive action behind trust:
const verdict = await gp.authorize("dashboard.example.com", { minTrust: 0.7 });
if (verdict.authorized) {
  document.cookie = `gp_token=${verdict.token}; Secure; SameSite=Lax`;
}

3. Verify on the server

// express
import { graypass, requireGrayPass } from "@graypass/middleware/express";
app.use(graypass({ apiKey: process.env.GRAYPASS_SECRET_KEY! }));

app.get("/dashboard", requireGrayPass({ minTrust: 0.7 }), (req, res) => {
  // requireGrayPass already returned 401 if req.graypass.valid was false
  // or trustAtIssue was below 0.7
  res.render("dashboard", { user: req.graypass.userId });
});

3a. Or in Next.js

// middleware.ts
import { withGrayPass } from "@graypass/middleware/nextjs";

export default withGrayPass({
  apiKey: process.env.GRAYPASS_SECRET_KEY!,
  minTrust: 0.4,
  target: "dashboard",
  stepUpRedirect: "/step-up",
  protectedPaths: ["/dashboard", "/admin"],
});

export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"] };

Auth and keys

Two key types per tenant.

Both types issue from the same self-service flow. Rotate either at any time from the portal.

API endpoint index

POST/v2/sessions/start begin a continuous session (billable)
POST/v2/sessions/{id}/frames submit one feature frame (free)
POST/v2/sessions/{id}/end end and summarize
GET/v2/sessions/{id}/telemetry live trust state
POST/v2/sessions/{id}/telemetry/ticket issue a stream ticket
GET/v2/sessions/{id}/telemetry/stream SSE telemetry (with ticket)
POST/v2/sessions/{id}/authorize issue zero-friction token (billable on success)
POST/v2/verify verify a token server-side
POST/v2/enroll save behavioral template
POST/v2/templates/rotate cancelable-template re-issue
GET/v2/policy read tenant policy
PUT/v2/policy update policy
GET/api/billing/plan current plan + usage + projection
GET/api/billing/usage per-event call breakdown
POST/api/billing/payg/enable turn on metered billing + add card

Sessions

Start a session

POST /v2/sessions/start
Authorization: Bearer pk_…
Content-Type: application/json

{ "user_id": "u_42" }

→ 200
{
  "session_id": "sess_…",
  "user_id":    "u_42",
  "enrolled":   true,
  "trust":      0.5,
  "state":      "initializing",
  "ws_url":     null,
  "created_at": "2026-05-16T20:00:00Z"
}

Submit a feature frame

Send a 15-second nested feature frame. The SDK builds these for you.

POST /v2/sessions/{id}/frames
Authorization: Bearer pk_…
Content-Type: application/json

{
  "frame_id": "f_abc",
  "timestamp": 1748382937.142,
  "duration_ms": 15000,
  "features": {
    "keystroke": { "dwell_mean": 86, "flight_mean": 102, ... },
    "pointer":   { "speed_mean": 0.42, ... },
    "scroll":    { "delta_mean": -34, ... },
    "focus":     { "tab_switch_count": 0, ... },
    "gaze":      { "fixation_rate": 3.1, ... }
  }
}

→ 200
{ "trust": 0.91, "state": "stable", "raw_scores": {...} }

End a session

POST /v2/sessions/{id}/end
→ { "session_id": "sess_…", "duration_s": 142.5, "final_trust": 0.94 }

Authorization

Issue a zero-friction token

POST /v2/sessions/{id}/authorize
Idempotency-Key: f4c2-… (optional; ≤ 128 chars)
{ "target": "settings.example.com", "min_trust": 0.7, "ttl_seconds": 300 }

→ { "authorized": true, "token": "<payload_b64url>.<hmac_b64url>", "trust": 0.92 }

ttl_seconds must be between 10 and 3600 (1 hour). Tokens are HMAC-signed and tenant-scoped. Format is two base64url segments joined by a dot - not a JWT. Verify on the receiving service with POST /v2/verify. Same-tenant only. Successful authorize calls are billable (see Billing).

Sending Idempotency-Key deduplicates retries: the first call within 60s computes the verdict and caches it, subsequent calls return the cached response. A second in-flight call with the same key gets 409 idempotency_in_progress.

Verify a token

POST /v2/verify
{ "token": "<payload_b64url>.<hmac_b64url>", "target": "settings.example.com" }

→ { "valid": true, "user_id": "<tenant_id>:u_42", "trust_at_issue": 0.92 }

user_id is the tenant-scoped form (tenant_id :: the value you passed to /v2/sessions/start). The Express + Next.js middleware expose this on req.graypass.valid and req.graypass.userId. Tokens are one-time-use - second verify of the same token returns valid: false.

Enrollment

POST /v2/enroll
{ "user_id": "u_42", "session_id": "sess_…" }

→ { "enrolled": true, "template_id": "tmpl_…" }

Rotate a template (cancelable biometrics)

POST /v2/templates/rotate
{ "user_id": "u_42" }

→ { "rotated": true, "template_id": "tmpl_…" }

If a template ever leaks, rotate the seed. The user's identity stays; the projected template behind it is replaced in milliseconds. The next session re-enrolls from live behavior under the new seed.

Policy

GET /v2/policy
PUT /v2/policy {
  "mode": "enforced",
  "rules": [
    { "state": "stable",    "action": "allow"   },
    { "state": "uncertain", "action": "observe" },
    { "state": "degraded",  "action": "step_up", "grace_period_s": 30 },
    { "state": "critical",  "action": "kill"    }
  ]
}

mode is shadow (observe only, no enforcement) or enforced (actual actions taken). Rules map a trust state to a recommended action. Optional grace_period_s, min_trust, max_trust, and webhook_url per rule.

Webhooks

Configure a callback URL in the portal. We POST signed events for trust.state_change, trust.degraded, trust.critical, and trust.recovered. Verify the X-GrayPass-Signature header with the secret configured on that webhook.

Failed deliveries retry with exponential backoff and full jitter, then land in the webhook queue for manual replay.

Telemetry stream

Polling GET /v2/sessions/{id}/telemetry with the secret key works for low-frequency dashboards. For real-time UIs (live trust gauge), use the SSE stream - it requires a short-lived ticket so you never have to leak the secret key into a URL.

POST /v2/sessions/{id}/telemetry/ticket
Authorization: Bearer sk_…
→ { "ticket": "tkt_…", "expires_in": 60 }

GET /v2/sessions/{id}/telemetry/stream?ticket=tkt_…
→ text/event-stream
data: { "trust": 0.94, "state": "stable", "frame_count": 7, ... }
data: { "trust": 0.91, "state": "stable", ... }

Streams are capped per-tenant per process (GRAYPASS_SSE_MAX_PER_TENANT, default 50). Over the cap returns 429. Clients should reconnect with the same ticket on transient disconnect.

Errors

Limits

Billing

GrayPass bills per call. One billable call is one successful POST /v2/sessions/start or one successful POST /v2/sessions/{id}/authorize (where authorized: true). Frames, verify, enroll, telemetry, and webhooks are free — send as many as your scoring needs.

Free tier — no card required

Pay-as-you-go — add a card to remove the cap

Graduated tiers, applied per call. The price drops as your volume rises.

Commit plans — predictable monthly + discount

Billing API

GET /api/billing/plan
→ {
  "billing_mode": "payg",
  "billable_calls": 4218,
  "included_calls": 1000,
  "projected_bill_usd": 25.74,
  "payment_method_on_file": true,
  "days_remaining_in_cycle": 14,
  "cycle_end_ts": 1748908799.0,
  "payg_tier_breakdown": [...]
}

GET /api/billing/usage
→ {
  "calls_this_cycle": 4218,
  "by_event": { "session_start": 1830, "authorize": 2388 },
  "projected_bill_usd": 25.74
}

POST /api/billing/payg/enable        // returns SetupIntent client_secret
POST /api/billing/checkout           // commit plan upgrade (Builder/Scale)
POST /api/billing/portal-link        // Stripe customer portal

Privacy

We never store the raw events you collect. The SDK reduces them to a 54-dimensional summary on the device, ships only the vector, and the backend persists templates, never traces. The OPRF protocol means the server never sees the unblinded behavioral vector either — templates are protected against server compromise. See privacy policy and security on graypass.org.