GRAYPASS DOC/API-01
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({
tokenProvider: async () => {
const response = await fetch("/graypass/client-token", {
method: "POST",
credentials: "include",
});
if (!response.ok) throw new Error("client token unavailable");
return await response.json();
},
});
const { session_id } = await gp.startSession({ userId: "u_42" });
gp.on("trust_change", ({ confidence, state }) => {
console.log("trust", confidence, 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. Mint browser credentials on the server
import { clientTokenRoute } from "@graypass/middleware/express";
app.post("/graypass/client-token", express.json(), requireLogin, clientTokenRoute({
apiKey: process.env.GRAYPASS_SECRET_KEY!,
getUserId: (req) => req.user.id,
}));
// Response fields match the JSON API:
// { client_token, publishable_key, expires_at, user_id, origin }
The backend derives the subject from its authenticated session and
mints a short-lived ct_*. Register the browser app's exact
HTTPS origin in the portal first; the middleware forwards that
Origin for token binding but never auto-registers it. The
SDK renews the token on each request and retries once after a 401. The
sk_* never reaches the browser.
3a. One-tag alternative
<script src="https://api.graypass.org/gp/graypass.js" data-pk="pk_live_…" data-token-url="/graypass/client-token" data-user-id="USER_ID_FROM_YOUR_SESSION" data-consent="popup" defer></script>
data-pk binds the tag to the keypair echoed by the token
endpoint; it also verifies that the returned origin equals
window.location.origin. The loader does not use
data-pk as an API bearer credential.
4. Verify authorization on the server
// express
import { graypass, requireGrayPass } from "@graypass/middleware/express";
const graypassConfig = {
apiKey: process.env.GRAYPASS_SECRET_KEY!,
target: "dashboard",
minTrust: 0.7,
};
app.use(graypass(graypassConfig));
app.get("/dashboard", requireGrayPass(graypassConfig), (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 });
});
4a. 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
Keep authentication, public identification, and signing separate.
sk_test_…/sk_live_…— server API credential. It mints client tokens and verifies authorization tokens. Never commit or embed it.pk_test_…/pk_live_…— public keypair identifier. The one-tag loader uses it to validate the token endpoint'spublishable_key; it is not the production bearer credential.ct_…— renewable, short-lived browser bearer bound to one tenant, user, parent key, exact browser origin, and route set.whsec_…— one-time-disclosed webhook signing secret. It verifies delivery signatures and is not an API key.
API keypairs issue from the self-service flow. Mint ct_*
only from an authenticated backend, and store each whsec_*
when a webhook is created or rotated because list/read responses never
reveal it.
API endpoint index
sk_* only)Sessions
Start a session
POST /v2/sessions/start
Authorization: Bearer ct_…
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 ct_…
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
Submit at least five distinct SDK frames with
purpose: "enroll" before completing enrollment. A frame
counts only when its per-session HMAC signature is valid and PAD,
replay, and non-finite checks are clean.
POST /v2/enroll
Authorization: Bearer ct_…
{ "user_id": "u_42", "session_id": "sess_…" }
→ { "enrolled": true, "user_id": "u_42", "template_id": "tmpl_…", "message": "enrolled_from_session" }
Rotate a template (cancelable biometrics)
POST /v2/templates/rotate
{ "user_id": "u_42" }
→ { "ok": true, "template_id": "tmpl_…", "template_hash": "…", "transform_id": "…", "transform_version": 1, "message": "V2 template rotated successfully" }
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. A policy webhook URL must also be
an active registration under Webhooks
so it can use that registration's disclosed signing secret.
Webhooks
Configure a public HTTPS callback URL and event types in the portal. We POST signed events for
trust.state_change, trust.degraded,
trust.critical, and trust.recovered.
Creation returns a unique whsec_... secret exactly once;
rotation replaces it and also shows the new value once. List responses
never contain secret material. Keep this separate from your
sk_... API key.
X-GrayPass-Signature-V2: v2=<HMAC-SHA256> X-GrayPass-Timestamp: <unix-seconds> X-GrayPass-Delivery-Id: evt_... X-GrayPass-Signature: sha256=<legacy-body-only-HMAC>
The v2 HMAC covers
timestamp + "." + delivery_id + "." + exact_raw_body_bytes.
Use the raw request bytes and reject stale timestamps. The middleware's
parseWebhook does this by default when v2 headers are
supplied. The body-only header remains temporarily for old consumers.
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_at": "2026-05-16T20:01:00Z", "session_id": "sess_…" }
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
400bad request. Schema validation failed.401unauthorized. Missing or invalid key.402payment required. Free-tier call quota exhausted, or subscription canceled / past-due. Body includes adetailwith the reason and the portal URL to add a card.403forbidden. Wrong tenant, revoked key, or scope-restricted (pk_*key on mutating route whenGRAYPASS_REQUIRE_KEY_SCOPE=1).404not found.409conflict. Used by/authorizewhen anIdempotency-Keyis in-flight or by template rotation race.413payload too large. Body over the configured cap (default 256 KiB).429rate limited. SeeX-RateLimit-Reset. Also returned by SSE telemetry stream when the per-tenant cap is hit.503service unavailable. Backing store unreachable, or webhook event still claimed by another worker.
Limits
- Rate limit: 1,000 req/h on test keys; 10,000 req/h on live keys; higher on enterprise. Set per-tenant in the portal.
- Request body: ≤ 256 KiB by default (
GRAYPASS_MAX_BODY_BYTES). Returns 413 above the cap. - Token
ttl_seconds: 10 seconds minimum, 3,600 seconds (1 hour) maximum. Idempotency-Key: ≤ 128 characters, cached for 60s after first verdict.- SSE telemetry streams: 50 concurrent per tenant per process (
GRAYPASS_SSE_MAX_PER_TENANT).
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
- 100 free sessions per calendar month.
- Session 101 returns
402(free_tier_exhausted) until you buy a session pack, add a card, or the next cycle starts. - Test keys ship with this by default. No upfront commitment.
Pay-as-you-go — add a card to remove the cap
Graduated tiers, applied per call. The price drops as your volume rises.
- 0–100 calls/mo — free
- 101–10,000 — $0.0120 / call
- 10,001–100,000 — $0.0060 / call
- 100,001–1,000,000 — $0.0025 / call
- 1,000,001+ — $0.0012 / call
- Prepaid credit packs — $99 / 20k, $399 / 100k, $999 / 350k, $2,499 / 1M (20–38% off PAYG). Buy in portal.
Commit plans — predictable monthly + discount
- Builder — $79/mo, 20,000 calls included, $0.005/call overage (breakeven ≈ 14,000 calls vs PAYG).
- Scale — $399/mo, 200,000 calls included, $0.0012/call overage (breakeven ≈ 125,000 calls).
- Enterprise — custom contract: SLA, BAA, single-tenant, dedicated CSM.
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.