A verified email, and a very small blast radius.
This page is the honest version: what verification guarantees, what it does not, and which half of the problem belongs to you.
The blast radius, and it is the thing to read twice
There is no consent model. A human authorizes their machine once, by creating a key. After that, an agent on that machine can request a token for any domain without asking again. We rejected per-service consent because it puts a browser prompt back in the loop, and an agent that stops to ask a human for every service is not autonomous.
The consequence is the one line the rest of this page depends on: any AgentOnboard user can present a genuine identity to your service whether or not they have ever heard of it. The token will verify. It is signed, it is short-lived, and the inbox in it was really checked.
What stops them is your account check. An identity with no existing account at your service must be rejected, with ACCOUNT_REQUIRED and a 403. Return nothing from your resolver for an email you do not already know. This is a documented contract, not an enforced one — we cannot enforce it for you, and a page that implied we could would be worse than no page.
Two ways to break that rule, and both look like helpfulness:
Auto-provisioning, and linking by email
Auto-provisioning turns your service into a writable surface for strangers who have never seen it: spam, scraping, a terms-of-service bypass, and support load you cannot attribute to anyone.
Linking an incoming identity to an existing account by matching on email is a permanent account-takeover path. Whoever controls the inbox at AgentOnboard has demonstrated nothing about owning the account at your service, and the link outlives the session.
Gate on the connection, not on the token. An agent can work inside an account; it cannot create one. If you need a service that serves only your existing customers, this model cannot serve you, and that is a design decision rather than a defect in either integration.
What a verified email is worth
Very little on its own. It means the inbox was verified at AgentOnboard. It does not mean the person is your customer, is not banned, is not already a different account in your database, or consented to anything. There is no email_verified claim, because a constant set to true at signing time is evidence of nothing.
The abuse surface you are accepting is authorized-but-unknown identities, spam, scraping, and terms-of-service bypass. It is yours to manage. Deny-by-default still holds underneath it: a forged, expired, or misaddressed token is still rejected.
What verification checks
Every failure is a rejection. There is no path in which an error yields a pass.
- Algorithm
- RS256 only — RSA 2048 or larger, SHA-256. The algorithm is pinned by the verifier. The token's own header never chooses it, so `alg: none` and an HS256 key-confusion attempt are rejected as `INVALID_SIGNATURE`.
- Audience
- `aud` is compared to your normalized hostname by full equality, never by prefix or suffix. `notes.com.evil.com` is not `notes.com`, and a token minted for one is rejected by the other with `AUDIENCE_MISMATCH`. Normalization drops scheme, path and port and lowercases; `localhost` keeps its port.
- Lifetime
- A token lives five minutes. `exp` is `iat + 300`, enforced with an explicit 5-second clock-skew tolerance, and the lifetime is not negotiable per request.
- Required claims
- `exp`, `jti`, `sub` and `email` must all be present and usable. A `kid` in the header is required to select a key, so a token with no `kid` is rejected rather than matched against whatever set happens to be loaded.
- Replay
- `jti` is the correlation key and is returned for your audit log. It is not a replay defence: the SDK records no token ids, so a live token presented twice inside its five minutes verifies both times. It is a bearer credential — TLS only, never logged.
- Key retrieval
- Public keys are served at `/.well-known/jwks.json` and cached for at most an hour. A fetched set is shape-checked before anything from it is cached, an unknown `kid` triggers at most one refetch per 30 seconds, and a failed fetch falls back to the last set that passed validation rather than rejecting. A cold process with a failing fetch is the one case that returns `KEY_SOURCE_UNAVAILABLE`.
Where the keys and the secrets are
The private key used to sign tokens never leaves the identity engine. Verification uses the public JWKS, so your request never passes through us. Private keys live in the root environment file and are not synced into any client bundle.
The master key a human creates is shown once and never again. We store a salted hash, and the local credential file it lands in is written with owner-only permissions. It never leaves the user’s machine, and it is never handed to an agent or to a service — the CLI is the only thing that holds it.
Revoking a key blocks future minting immediately, across every device. A token already issued keeps verifying until it expires, up to five minutes; a signing key retired from the JWKS is the other mechanism, and it stops verification at once with UNKNOWN_KEY. The two are different things and the SDK does not conflate them.
What verification costs you
Verification is local. There is no introspection call, no per-request callback into us, no database on the verification path, and one runtime dependency. The only network step is retrieving the key set, and it is bounded: cached for an hour, one refetch per 30 seconds on an unknown kid, and a fallback to the last validated set when a fetch fails.
The honest caveat: a key-endpoint outage is added latency, up to about five seconds per request, not a rejection — and a process that starts cold during that outage fails with KEY_SOURCE_UNAVAILABLE. Verification results are never cached, because a cached “this token is valid” is how a revoked token keeps working.
What we do not have
No SOC 2 or ISO 27001. No penetration test. No uptime or availability history. No status page, no security.txt, no bug bounty. There is no partner console, so there are no partner audit logs on our side either. If a questionnaire asks for any of those, the answer today is that it does not exist, and you should price that accordingly.
We also do not log raw tokens or the Authorization header, and there is nothing for us to log: they never reach us. The per-request record is yours, and the fields worth keeping are the error code and the jti.
Two claims are worth stating narrowly rather than broadly, because a security page that overstates is worse than no page. “Runs anywhere” means the package imports no Node builtin and nothing but jose — a property of the built artifact, checked on every release gate, not a runtime observation. And “no socket” means the SDK never imports node:http or node:net; it builds a URL and hands it to your runtime’s fetch.
The parts we hold and the parts you hold
We hold the key, the signature, the five-minute window, and the verified inbox. You hold the account, the plan, the ban, and the decision. Neither half is negotiable from this side.
Every check above, and the code that runs them.
The verifier, the resolver contract, the failure codes, and the deployment checklist.