Authentication and delegation
Gungner uses OAuth2 client-credentials. A tenant grants a consumer — an application, a
partner integration, or your own backend — a set of scopes. The consumer authenticates as
itself (client_id / client_secret), not as an end user, and everything it does happens
under the tenant’s delegation.
Two rules that shape everything else:
- One tenant per consumer. A
client_id/client_secretpair is valid for exactly one tenant. There’s no cross-tenant credential — if you work with multiple tenants, you hold a separate credential pair for each. - Every access is logged. Each call a consumer makes is recorded against that consumer and that tenant, so activity is auditable after the fact.
Scopes
Section titled “Scopes”A grant is a set of scopes, and scopes gate what a consumer can reach — over REST and over MCP identically, since MCP tools map onto the same routes under the same token.
| Scope | Unlocks |
|---|---|
identity:read |
Declared in the scope catalogue; not yet required by any live route. |
events:read |
Reading the event log. |
kyb:read |
Company data — profiles, officers, signatories, beneficial owners, engagements. |
kyb:refresh |
The paid company refresh — POST /v1/companies/{orgnr}/refresh and the refresh_company tool — and nothing else. Since interface 1.6.0, kyb:read no longer opens it. |
kyc:read |
Person data — profiles only. |
monitoring:read |
Reading monitoring subscriptions — the read-only half of monitoring:subscribe. |
monitoring:subscribe |
Creating, reading, and ending monitoring subscriptions. |
screening:read |
Reading a screening check that already exists. |
screening:run |
Running a new screening check — a write scope, since a check is an append-only, evidence-bearing result; it also lets you read back the check you just ran. |
decisions:write |
Writing back risk decisions and flags. Declared in the scope catalogue; not yet required by any live route. |
A consumer only ever sees what its scopes cover — a token minted with just kyb:read, for
example, can’t reach person data, over REST or MCP.
Minting and using a token
Section titled “Minting and using a token”curl -s -X POST https://api.gungner.ai/oauth/token \ -H 'content-type: application/json' \ -d '{ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" }'{ "access_token": "…", "token_type": "Bearer", "expires_in": 600 }Send it as Authorization: Bearer <access_token> on every subsequent call. A small reusable
TypeScript client:
interface GungnerClientOptions { baseUrl: string; clientId: string; clientSecret: string;}
function createGungnerClient({ baseUrl, clientId, clientSecret }: GungnerClientOptions) { let token: { value: string; expiresAt: number } | undefined;
async function getToken(): Promise<string> { if (token && token.expiresAt > Date.now()) return token.value; const response = await fetch(`${baseUrl}/oauth/token`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret }), }); const body = await response.json(); token = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 - 30_000 }; return token.value; }
return { async get(path: string) { const accessToken = await getToken(); return fetch(`${baseUrl}${path}`, { headers: { authorization: `Bearer ${accessToken}` } }).then((r) => r.json()); }, };}Token lifetime
Section titled “Token lifetime”Tokens are short-lived. Re-mint before a token expires rather than holding one indefinitely —
the client above does this by tracking expires_in and refreshing a little before the
deadline, which is the pattern to follow in your own integration too.
What’s next
Section titled “What’s next”- Getting started — the shortest path from credentials to a working call.
- Using the MCP server — the same token, the same scopes, a different transport.
- Subject ID — the identifier every scoped response carries.