> ## 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.

# Business Agents

> Know which business an agent acts for and which person authorized it, checked together.

An agent can act for a business instead of a single person. It then presents a business credential. At L3 it also presents the credential of the person acting for the business, so you know both who the company is and who is behind this agent.

## Business credentials at L2 and L3

A business L2 credential can be presented alone. At L3, the business and person credentials are presented and checked together. A business L3 credential presented alone is refused with `ACTOR_MISMATCH`. Pass the person's credential to `verify` as `actor`.

## How the pair is presented

Both credentials are presented under one nonce, signed by the same agent key. In a token exchange, the business credential arrives as `subject_token` and the person's credential as `actor_token`.

The two are checked as one unit. If either fails, the whole presentation is refused.

## What you get

| Claim | On a business pair |
| - | - |
| `sub` | The business's stable ID at your site. |
| `act.sub` | The acting person's ID. |
| `verification.active_keys` | The checks that passed for the business only. |
| `act.verification.active_keys` | The checks that passed for the person only. |
| `exp` and `act.exp` | Each credential's expiry. `toTokenClaims().exp` is the earlier. |
| `claims` | Fields shared about the business. |
| `act.claims` | Fields shared about the person. |
| `act.authorization` | How Baselayer confirmed the person may act for the business. |

Python uses the same names, with `to_token_claims()` and `actor_attests()`.

## What the pair checks add

On top of the normal checks for each credential, the SDK checks:

| Check | Failure |
| - | - |
| Both credentials come from the same issuer | `ACTOR_MISMATCH` |
| Both are bound to the same agent key | `KEY_BINDING_INVALID` |
| Both use the same nonce | `KEY_BINDING_INVALID` |
| The business credential names this person as its actor | `ACTOR_MISMATCH` |
| Baselayer has verified that this person may act for the business | `ACTOR_NOT_AUTHORIZED` |

## Verify the pair

> [!NOTE]
>
> The KYA SDK is coming soon. The packages aren't published yet, so the code below won't run until they are.

Use `verifier.verify(subjectToken, { actor: actorToken, scope })`. Pass the person's credential as `actor` when there is one, and the scope from your published profile, as in [Asking for Fields](https://docs.baselayer.com/docs/kya-asking-for-fields.md). Passing the scope is what makes your site refuse credentials that share more than your profile lists.

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

// POST /oauth/token. actorToken is the actor_token field of the form body, when sent.
// scopeReference is the scope field. profile is your parsed kya-profile.json.
async function businessTokenEndpoint(
  subjectToken: string,
  scopeReference: string,
  actorToken?: string,
) {
  const disclosureScope = profile.resolveReference(scopeReference);
  if (disclosureScope === null) return oauthError(VerificationFailure.UnknownDisclosureScope);

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

  const claims = report.exchangeClaims();
  // Without an actor, verify accepts any credential, so confirm it is a business one.
  if (claims.credentialType !== CredentialType.Business) {
    return oauthError(VerificationFailure.UnsupportedCredentialType);
  }
  // attests() asks the business only. actorAttests() asks the person. Require both.
  const screened = claims.attests("watchlist_clear") && claims.actorAttests("watchlist_clear");
  const scope = screened ? "payments" : "browse";
  // Your token carries sub (the business) and act.sub (the person).
  return tokenResponse(claims, scope);
}
```

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


# POST /oauth/token. actor_token is the actor_token field of the form body, when sent.
# scope_reference is the scope field. profile is your parsed kya-profile.json.
async def business_token_endpoint(
    subject_token: str, scope_reference: str, actor_token: str | None
):
    disclosure_scope = profile.resolve_reference(scope_reference)
    if disclosure_scope is None:
        return oauth_error(VerificationFailure.UnknownDisclosureScope)

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

    claims = report.exchange_claims()
    # Without an actor, verify accepts any credential, so confirm it is a business one.
    if claims.credential_type is not CredentialType.Business:
        return oauth_error(VerificationFailure.UnsupportedCredentialType)
    # attests() asks the business only. actor_attests() asks the person. Require both.
    screened = claims.attests("watchlist_clear") and claims.actor_attests("watchlist_clear")
    scope = "payments" if screened else "browse"
    # Your token carries sub (the business) and act.sub (the person).
    return await token_response(claims, scope)
```

`oauthError` and `tokenResponse` are the helpers from the [SDK quickstart](https://docs.baselayer.com/docs/kya-sdk-quickstart.md#the-two-helpers).

## Checking what Baselayer verified

Each credential carries its own checks. `claims.attests(key)` is true when the business has the check. It says nothing about the person. `claims.actorAttests(key)` is true when the person has the check.

For any rule that should cover both, such as a clear watchlist result, require both. If you check only `attests()`, a screened business lets through a person who was never screened.


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