Reference
API reference
The hosted Identity API, and every option the middleware takes.
Base URL https://api.wayleave.dev · Auth
Authorization: Bearer <key>, keys from
your dashboard. Requests and responses
are JSON. Every response carries a request_id.
POST /v1/identify
Classify one request from its headers. Stateless: Wayleave never fetches the URL you name. It is a string to be matched against, not a thing to go and get.
Body
| Field | Type | Notes |
|---|---|---|
url | string, required | the full URL the request was made to |
headers | object, required | the request’s headers as received; names are case-insensitive |
method | string | defaults to GET |
ip | string | client IP, if you trust your proxy to supply it |
Response 200
{
"lane": "verified_agent",
"verified": true,
"identity": {
"operator": "Example Labs",
"agentId": "…",
"keyid": "…",
"source": "https://…/http-message-signatures-directory"
},
"signature": { "present": true, "valid": true, "reason": null,
"expires": "2026-09-28T…", "nonce_fresh": true },
"reasons": ["crypto: valid signature"],
"request_id": "…",
"latency_ms": 0.8
}
lane is one of verified_agent, declared_agent,
suspected_bot, human. verified is true only for a valid Ed25519
HTTP Message Signature (RFC 9421, Web Bot Auth). A replayed single-use signature
comes back verified: false with nonce_fresh: false. identity is
null unless verified — an unverified identity is a claim, and this API does
not repeat claims as facts.
Errors
| Status | Meaning |
|---|---|
400 | body missing url or headers, or not valid JSON |
401 | missing or rejected API key; answers WWW-Authenticate: Bearer |
413 | body larger than 64 KB |
429 | over your per-key rate limit; Retry-After in seconds |
Every 200 carries x-ratelimit-remaining.
POST /v1/identify/batch
The same, for 1–100 requests at once. Each item counts against your rate limit.
{ "requests": [ { "url": "…", "headers": { … } }, … ] }
Response: { "results": [ …one per request, in order… ], "count": n, "request_id": "…" }
Webhooks — first-seen identity
When configured for your key, Wayleave POSTs the first time a verified identity is seen:
{ "type": "identity.first_seen", "at": "…", "identity": { … }, "lane": "…" }
Headers: x-wayleave-timestamp, and
x-wayleave-signature: v1=<hex HMAC-SHA256 of "<timestamp>.<body>" with your webhook secret>
Reject signatures older than five minutes, or a captured delivery can be replayed at leisure. HTTPS endpoints only.
GET /healthz
{ "ok": true } — no auth.
Security model
Keys are stored as SHA-256 hashes and compared in constant time. The API
never fetches the URLs it is asked about, so it cannot be turned into a request
forwarder. Responses carry nosniff, X-Frame-Options: DENY, HSTS and
no-store.
Report a vulnerability to gibran@wayleave.dev. Please give us a way to reach you back.
Middleware reference — wayleave on npm
new Wayleave(options)
| Option | Type | Meaning |
|---|---|---|
pricedPaths | { [prefix]: usd } | HTTP 402 for non-human lanes under these prefixes |
strictPricedPaths | boolean | only confirmHuman callers pass a priced route free |
confirmHuman | (req) => boolean | your own signal that a person is present, usually a session |
verifyPayment | verifier | e.g. coinbaseFacilitator({…}) from wayleave/x402; absent means priced routes stay at 402 |
rules | { [lane]: [[prefix, allow], …] } | first match wins |
rateLimits | { [lane]: perMinute } | fixed window |
directories | directories or resolver | which signing keys to trust |
policy | { url, publicKey } | a signed policy document fetched and verified at runtime |
meter | { apiKey } | send crossings to your dashboard |
store, sink | interfaces | swap replay and rate storage, or the event sink |
onWarn | (message) => void | where startup warnings go; defaults to console.warn |
gate.express() returns Express middleware. gate.handleAsync(req) returns
{ status, lane, why } for any other framework; gate.handle(req) is its
synchronous twin and cannot evaluate a remote policy or await a network payment
verifier. Full types ship in index.d.ts.