🧪 Beta — Available today; the interface contract may still change.
Your first delegation in 30 minutes
By the end of this guide you will have:
- A Delegate Token issued because the user approved it in person — not a password, not a long-lived API key, but a short-lived, attenuated, revocable slice of authority
- One complete verification and product call: confirm the token is valid, then call Web Search through the SDK
- A traceable audit chain
auditId— who granted it, what they granted, for how long, all on the record
Before you start: two things to get straight
① Why endpoints say eak and not genauth: GenAuth is Qoni's identity layer. The current SDK package is @qoniai/qoni; server paths under /api/v3/eak/... remain as a wire compatibility boundary (details in What is GenAuth).
② What stands in for "the resource being accessed": the example calls Qoni Web Search with webagent.web_search:read and webagent.web_search:manage, so you do not need to modify your own API first.
Step 0 — Get an access key (3 min)
- Open your workspace at dashboard.qoni.ai
- Credentials → Create, and configure two allowlists:
allowedScopes: the ceiling on which scopes this key may delegate. This tutorial needswebagent.web_search:manageandwebagent.web_search:read, so include at least those two.allowedAgents: the agent identifiers you allow delegation to. This tutorial usesreport-agent, so put it in.
- Copy the AccessKey / SecretKey. The secret is shown once. Lose it and your only option is to rotate and recreate
- Register your callback URL (
redirectUri) in the same place; this tutorial useshttp://localhost:3000/qoni/callback
export QONI_ACCESS_KEY=ak_demo_xxxxxxxxxxxxxxxx
export QONI_SECRET_KEY=sk_demo_xxxxxxxxxxxxxxxxKeys belong on the server and nowhere else
The AK/SK never goes into a browser, an app, or any client-side code. It can start delegations on behalf of your whole organization — storage requirements are in Security considerations.
Where the agent identifier (agent) comes from
This tutorial runs on the bare string report-agent, which works as long as it is in the allowedAgents list from the previous step. In production, an agent should be registered into the ledger first — with a description, an Owner, and a Sponsor — so the identifier maps to a registration record. See The Agent Identity model.
Step 1 — Install the SDK (30 sec)
npm install @qoniai/qoni# Nothing to install. But calling HTTP directly means building the AK/SK-signed
# Authorization header yourself; the cURL snippets here use <AK/SK signature> as a placeholder.
# To go direct: generate it with the SDK's exported buildStringToSign / buildSignature /
# buildAuthorization functions, or get it working with the SDK first and migrate later.
# See h2-sdk-reference and h1-api-reference.The cURL snippets illustrate the contract; they are not paste-and-run commands
The signing algorithm is not covered on this page. To get running in 30 minutes, take the TypeScript path; use the cURL snippets to check the HTTP contract against.
Step 2 — Start the delegation and get an authorization link (8 min)
interactive mode: you make the request on the agent's behalf, and it counts only when the user says so.
import { Qoni, QoniScopes } from "@qoniai/qoni";
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
});
const { data } = await qoni.delegateToken({
mode: "interactive",
agent: "report-agent",
scopes: [QoniScopes.WEB_SEARCH_MANAGE, QoniScopes.WEB_SEARCH_READ],
redirectUri: "http://localhost:3000/qoni/callback",
state: "demo-state-001",
});
const authorizationUrl = new URL(data.authorizationUrl);
if (authorizationUrl.searchParams.get("grant_id") !== data.grantId) {
throw new Error("Invalid delegation grant");
}
// The callback does not carry grantId: save it keyed by grantState and look it up by grant_state.
await savePendingGrant(data.grantState, {
grantId: data.grantId,
businessState: "demo-state-001",
});
console.log(data.authorizationUrl); // send the user here
console.log(data.grantId); // saved with grantState; the exchange needs itcurl -X POST "$QONI_HOST/api/v3/eak/delegations" \
-H "Authorization: <AK/SK signature>" \
-H "Content-Type: application/json" \
-d '{
"mode": "interactive",
"agent": "report-agent",
"scopes": ["webagent.web_search:manage", "webagent.web_search:read"],
"redirectUri": "http://localhost:3000/qoni/callback",
"state": "demo-state-001"
}'The authorizationUrl in the response is the entry point to the consent page. Response state is a compatibility alias of server-generated grantState, not an echo of caller input; the stable correlation contract is the URL query grant_id equal to response grantId. Scopes use the service.capability:action format — this example asks for running and reading web search, and nothing else.
Two places people get stuck
redirectUri must match what you registered in Step 0, or the request is rejected. For local development, http://localhost:3000/... is fine — you do not need public HTTPS.
Interactive does not pass user: Qoni Console resolves the signed-in user during authorization. Only silent mode requires user: { id: <genauth-user-id> }.
How the two spellings line up
In silent mode, the SDK uses user: { id }, which the HTTP contract translates to userId. Top-level SDK userId is deprecated; interactive mode passes neither form (see SDK Reference).
Step 3 — The user approves, you exchange for a Delegate Token (10 min)
The user opens authorizationUrl, signs in, and sees the consent page: which agent, which permissions, for how long. Once they approve, the browser bounces back to your redirectUri with a one-time code, your business state, and grant_state. The callback does not carry grantId, so look up the record you saved by grant_state. Finish the exchange in your callback handler:
// Your callback route (Express below; other frameworks read the query much the same way)
// GET /qoni/callback?code=...&state=<business state>&grant_state=... (no grantId)
app.get("/qoni/callback", async (req, res) => {
const query = req.query as Record<string, string>;
// Look up the record saved when the request was created; a miss means an invalid or used callback.
const pending = await loadPendingGrant(query.grant_state);
// Your business state comes back unchanged; compare it with the saved value to reject forged or mixed-up callbacks.
if (!pending || query.state !== pending.businessState) return res.status(400).send("state mismatch");
const { data: grant } = await qoni.completeDelegateToken({
grantId: pending.grantId,
code: query.code,
state: query.grant_state,
});
console.log(grant.token); // the Delegate Token
console.log(grant.expiresIn); // lifetime, in seconds
console.log(grant.auditId); // audit chain ID — every step from here is on the record
res.send("Authorized");
});curl -X POST "$QONI_HOST/api/v3/eak/delegations/complete" \
-H "Authorization: <AK/SK signature>" \
-H "Content-Type: application/json" \
-d '{ "grantId": "grant_xxx", "code": "code_xxx", "state": "grant_state_xxx" }'The code is single-use
The authorization callback code dies the moment it is consumed; replaying it fails outright. The token you get back is the thing you use from here on.
Step 4 — Verify and call Web Search (8 min)
First confirm the token is genuinely valid — introspect echoes back every fact about the grant:
const { data: info } = await qoni.genauth.introspectDelegationToken({
token: grant.token,
});
// { active: true, sub: "usr_demo_0001", agent_id: "report-agent",
// scope: ["webagent.web_search:manage", ...], grant_id, audit_id, ... }Why introspect returns snake_case
introspect echoes the claims inside the token, using agent_id, audit_id, and scope. The delegation response uses camelCase auditId for audit metadata but does not contain the effective scope list; use introspection scope when you need the actually granted boundary (full field table in Token and claim reference).
Pass grant.token directly to the product method. The SDK performs the internal product token exchange; application code should not call /api/v3/eak/token-exchange itself:
const search = await qoni.webSearch.run({
token: grant.token,
prompt: 'Qoni Agent Identity documentation',
maxResultsPerQuery: 5,
})
const result = await search.wait()
console.log(result.output)What happens next
- Tokens expire:
expiresInends it on the dot (60 seconds to 24 hours, whatever you asked for). For task-level delegation, keep the lifetime short. - Revocable at any time: for the layers of revocation and how fast each takes effect, see Revocation and emergency response.
- Queryable throughout: take the
auditIdto Audit and accountability chain.
Trusted server-side integration (no per-task user consent)
Your app already has a login system and you want to issue tokens for users in the organization's name? Take the silent path backed by organization-level consent — but first read Integrate your existing authentication system and the hard-constraint list in Security considerations.
Next steps
- Guide: Let an agent call your APIs on behalf of a user
- Concept: Delegate Token and attenuation
- Reference: API Reference, SDK Reference