> ## Documentation Index
> Fetch the complete documentation index at: https://docs.baselayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# KYA SDK Quickstart

> Start checking KYA credentials at your site: install the SDK, set it up and verify your first one.

The KYA SDK checks a KYA credential in your own code. It comes in TypeScript and Python. Both are tested against the same shared test data, so they give the same answer for the same credential.

You will build four things, in this order:

1. [Install](#install) the SDK.
2. [Set up one verifier](#set-up-a-verifier).
3. [Add the two routes](#add-the-two-routes): a nonce route (`POST /nonces`) and a token route (`POST /oauth/token`).
4. [Ask agents for a credential](#ask-agents-for-a-credential) by answering protected pages with `401` and a `KYA-Disclosure-Scope` header.

You also need a profile published at `/.well-known/kya-profile.json`. Every accepting site needs one. See [Asking for fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md).

## Install

> [!NOTE]
>
> The KYA SDK is coming soon. The install commands and the code on this page won't work until the packages are published. Until then, you can check credentials yourself with the [Credential Format Reference](https://docs.baselayer.com/docs/credential-format-reference.md).

```bash TypeScript theme={null}
# Coming soon: not published yet
npm install @baselayer/kya
```

```bash Python theme={null}
# Coming soon: not published yet
pip install baselayer-kya
```

TypeScript runs anywhere with WebCrypto Ed25519 and `fetch`: Node 20+, Auth0 Actions, Cloudflare Workers and other edge runtimes. Python needs 3.11 or later.

Method and option names in these docs are shown in TypeScript. The Python SDK uses the same names in snake\_case, such as `actorAttests` and `actor_attests`; the Python tab of each code sample shows the exact call.

The TypeScript SDK also covers the agent's side: the agent key, Key Binding JWTs, the token exchange with a site and DPoP-bound sessions. The Python SDK checks credentials only.

* TypeScript depends only on `libphonenumber-js`.
* Python depends on `certifi`, `cryptography`, `httpcore`, `httpx`, `idna`, `phonenumbers`, `pydantic` and `rfc8785`.

## Set up a verifier

Create one verifier and share it across requests. It holds the key cache, the revocation list cache and the single-use nonces.

Every verifier needs a nonce store, and there is no default.

* `InMemoryNonceStore` is for one long-lived process and for tests.
* If you run more than one server, serverless functions or edge workers, pass a store they all share instead. See [Share nonces across servers](https://docs.baselayer.com/docs/kya-where-to-run-it.md#share-nonces-across-servers).

```ts TypeScript theme={null}
import { InMemoryNonceStore, KyaVerifier } from "@baselayer/kya";

const verifier = new KyaVerifier({
  // Your domain, exactly as credentials are minted for you.
  audience: "shop.example",
  // One process only. Several servers need a shared store.
  nonceStore: new InMemoryNonceStore(),
  // Sandbox credentials are test data and are refused by default.
  // allowSandbox: true,
});
```

```python Python theme={null}
from baselayer_kya import InMemoryNonceStore, KyaVerifier

verifier = KyaVerifier(
    # Your domain, exactly as credentials are minted for you.
    audience="shop.example",
    # One process only. Several servers need a shared store.
    nonce_store=InMemoryNonceStore(),
    # Sandbox credentials are test data and are refused by default.
    # allow_sandbox=True,
)
```

Turn on `allowSandbox` (TypeScript) or `allow_sandbox` (Python) while you test with sandbox credentials. Leave it off in production.

## Add the two routes

The agent doesn't sign in with a password. It trades its credential for your own access token, the same way an app signs in with OAuth:

1. It calls your nonce route to get a one-time code, so a copied credential can't be reused. Each nonce works once.
2. It sends its credential to your token route, which checks it and returns your token.

The token route passes the scope from your published profile to `verify`. That is what makes your site refuse a credential that shares more than your profile lists.

```ts TypeScript theme={null}
import { DisclosureProfile, VerificationFailure } from "@baselayer/kya";

// POST /nonces
async function nonceEndpoint(): Promise<Response> {
  return Response.json({
    nonce: await verifier.issueNonce(),
    audience: verifier.audience,
  });
}

// The same JSON you publish at /.well-known/kya-profile.json.
const profile = DisclosureProfile.parse(JSON.parse(profileJson));

// POST /oauth/token. subjectToken and scopeReference are the
// subject_token and scope fields of the form body.
async function tokenEndpoint(subjectToken: string, scopeReference: string): Promise<Response> {
  const scope = profile.resolveReference(scopeReference);
  if (scope === null) return oauthError(VerificationFailure.UnknownDisclosureScope);

  const report = await verifier.verify(subjectToken, { scope });
  if (!report.accepted) return oauthError(report.failure);

  const claims = report.exchangeClaims();
  // Your policy decides from here.
  return tokenResponse(claims);
}
```

```python Python theme={null}
from baselayer_kya import DisclosureProfile, VerificationFailure

# The same JSON you publish at /.well-known/kya-profile.json.
profile = DisclosureProfile.model_validate_json(profile_json)


# POST /nonces
async def nonce_endpoint():
    return {"nonce": await verifier.issue_nonce(), "audience": verifier.audience}, 200


# POST /oauth/token. subject_token and scope_reference are the
# subject_token and scope fields of the form body.
async def token_endpoint(subject_token: str, scope_reference: str):
    scope = profile.resolve_reference(scope_reference)
    if scope is None:
        return oauth_error(VerificationFailure.UnknownDisclosureScope)

    report = await verifier.verify(subject_token, scope=scope)
    if report.failure is not None:
        return oauth_error(report.failure)

    claims = report.exchange_claims()
    # Your policy decides from here.
    return await token_response(claims)
```

### The two helpers

`oauthError` turns a failure into an OAuth error response. `tokenResponse` issues your own access token. The other pages in this section reuse both.

```ts TypeScript theme={null}
import { type ExchangeClaims, toOAuthError, type VerificationFailure } from "@baselayer/kya";

// The SDK picks the status and body for each failure. If Baselayer could not
// be reached, it is a retryable 503. The failure code always goes in kya_error.
function oauthError(failure: VerificationFailure): Response {
  const answer = toOAuthError(failure);
  return Response.json(answer.body(), { status: answer.status, headers: answer.headers() });
}

// Answer with your own access token, as any OAuth token route does.
async function tokenResponse(claims: ExchangeClaims, scope?: string): Promise<Response> {
  // Never let your token outlive the credential (or, on a business pair, either credential).
  const exp = claims.act !== null && claims.act.exp < claims.exp ? claims.act.exp : claims.exp;
  const expiresIn = Math.floor((exp.getTime() - Date.now()) / 1000);
  const accessToken = await issueAccessToken({ // your existing token code
    ...claims.toTokenClaims(), // sub and exp (plus act on a business pair)
    checks: claims.verification.active_keys, // the checks that passed, e.g. "phone_possession"
    scope,
  });
  return Response.json({
    access_token: accessToken,
    issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
    token_type: "Bearer",
    expires_in: expiresIn,
  });
}
```

```python Python theme={null}
import time

from baselayer_kya import ExchangeClaims, VerificationFailure


# The SDK picks the status and body for each failure. If Baselayer could not
# be reached, it is a retryable 503. The failure code always goes in kya_error.
def oauth_error(failure: VerificationFailure):
    answer = failure.to_oauth_error()
    return answer.body(), answer.status


# Answer with your own access token, as any OAuth token route does.
async def token_response(claims: ExchangeClaims, scope: str | None = None):
    # Never let your token outlive the credential (or, on a business pair, either credential).
    exp = min(claims.exp, claims.act.exp) if claims.act is not None else claims.exp
    expires_in = int(exp.timestamp() - time.time())
    access_token = await issue_access_token({  # your existing token code
        **claims.to_token_claims(),  # sub and exp (plus act on a business pair)
        # The checks that passed, e.g. "phone_possession".
        "checks": list(claims.verification.active_keys),
        "scope": scope,
    })
    return {
        "access_token": access_token,
        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "token_type": "Bearer",
        "expires_in": expires_in,
    }, 200
```

`issueAccessToken` stands for the code you already use to issue access tokens.

`report.checks` lists every check that ran, pass or fail. Write it to your audit log.

Related:

* For retries and outages, see [Handling failures](https://docs.baselayer.com/docs/kya-handling-failures.md).
* To tie your token to the agent's key with DPoP, see [Where to run it](https://docs.baselayer.com/docs/kya-where-to-run-it.md#in-your-login-server).
* On more than one server, [share nonces across servers](https://docs.baselayer.com/docs/kya-where-to-run-it.md#share-nonces-across-servers).

## Ask agents for a credential

When an agent with no token asks for a page that needs a verified person, answer `401` with a `KYA-Disclosure-Scope` header. The value is `<profile_id>#<scope_name>` from your published profile:

```http theme={null}
HTTP/1.1 401 Unauthorized
KYA-Disclosure-Scope: shop-example#returning-customer
```

The agent reads your profile, gets a credential minted with that scope's fields, and comes back through your two routes. See [Asking for fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md).

## What gets checked

The SDK runs these checks in order and stops at the first failure:

1. The presentation is well formed.
2. It is a KYA credential.
3. Baselayer issued it, and Baselayer's signing keys can be fetched.
4. The signature is valid and the credential is inside its time window.
5. It hasn't been revoked.
6. The shared fields match what Baselayer signed.
7. It is being presented by the agent it was issued to, with a nonce you issued, for your domain.
8. It was issued for your domain, its verification summary matches the checks it lists, and it shares no more than the scope you asked for (when you pass one).

On a pass, `report.exchangeClaims()` gives:

| Field | What it holds |
| - | - |
| `sub` | The customer ID. |
| `verification.active_keys` | The checks that passed. |
| `claims` | The shared fields, nested as issued. |
| `exp` | The expiry. |

`report.credential.disclosed` has the same shared fields as `{ path: value }`.

Each failure has its own code. See [Handling failures](https://docs.baselayer.com/docs/kya-handling-failures.md).

## Next steps

* [Where to run it](https://docs.baselayer.com/docs/kya-where-to-run-it.md): your backend, your login server or the edge, plus production tips.
* [Asking for fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md): limit what an agent can share.
* [Handling failures](https://docs.baselayer.com/docs/kya-handling-failures.md): what each refusal means and what to tell the agent.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.