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

# Matching Customers

> Connect an agent to the customer account you already have for that person.

There are two ways to know who an agent's customer is: the stable customer ID, and hashes you can compare against your own records.

## The stable customer ID

`claims.sub` is a stable ID for this customer at your site. The same person gets the same ID on every visit. Another site gets a different ID for the same person.

1. Store it against the customer's account.
2. On the next visit, look it up in your own database.
3. If you find it, this is a returning customer.

The verifier keeps no record of who it has seen.

> [!WARNING]
>
> Don't key customers on `claims.jti`. It is different on every credential.

## Hashes

A credential can share a value as a hash instead of in plain text, for example `hashes.email`. The hash looks like `sha256:<hex>`.

1. Hash the value you have on file with the SDK.
2. Compare it with the shared hash.
3. If they match, the customer's value is the same as yours, and you never received the plain text.

> [!WARNING]
>
> A matching hash is a clue that a value matches one you already hold. It is not proof that the agent owns the existing account. Confirm ownership through your normal sign-in or recovery flow before linking the account.

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

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

if (report.accepted) {
  // Hashing uses WebCrypto, so it is async.
  const isMatch =
    (await emailHash(emailOnFile)) === report.credential.disclosed["hashes.email"];
}
```

```python Python theme={null}
from baselayer_kya import email_hash

if report.failure is None:
    is_match = email_hash(email_on_file) == report.credential.disclosed.get("hashes.email")
```

The SDK cleans up each value the same way Baselayer does before hashing (for example, it lowercases emails and turns phone numbers into international format), so small formatting differences still match.

| Value | TypeScript | Python | Shared as |
| - | - | - | - |
| Email | `emailHash(email)` | `email_hash(email)` | `hashes.email` |
| Name | `nameHash(name)` | `name_hash(name)` | `hashes.name` |
| Phone | `phoneHash(phone)` | `phone_hash(phone)` | `hashes.phone_number` |
| Address | `addressHash({ line1, line2, city, state, postalCode, country })` | `address_hash(line1=..., line2=..., city=..., state=..., postal_code=..., country=...)` | `hashes.address` |
| EIN | `einHash(ein)` | `ein_hash(ein)` | `hashes.ein` (business credentials) |
| SSN | `ssnHash(ssn)` | `ssn_hash(ssn)` | `hashes.ssn` |

Baselayer fills in `hashes.ssn` from its own verification, and in Baselayer's hosted flow the person never types an SSN. `ssnHash` is only for matching against an SSN you already hold on file.

A few details:

* Phone numbers without a country code are read as US numbers.
* Address fields are all optional. `country` defaults to `"US"`. Pass the parts you have, already in standard USPS form.
* EIN and SSN keep digits only, so `12-3456789` and `123456789` hash the same.
* In TypeScript every hash function is async. In Python they are not.

Where to read the shared hashes:

| Credential | From the report | From `exchangeClaims()` |
| - | - | - |
| A single credential | `report.credential.disclosed["hashes.email"]` | `claims.claims.hashes.email` |
| Business, on a business pair | `report.business.credential.disclosed` | `claims.claims` |
| Acting person, on a business pair | `report.actor?.credential.disclosed` | `claims.act.claims` |

## Linking an agent to an existing account

As the warning above says, a matching hash is not proof of ownership. Email addresses get recycled, and the credential does not say the email is still theirs.

| Situation | What to do |
| - | - |
| `claims.sub` is already linked to an account | Sign the agent in to that account. |
| No link yet, and the email hash matches exactly one account | Confirm ownership first, with an email to that address or your normal sign-in. Then save `claims.sub` on the account. Until then, don't grant that account's access. |
| The hash matches several accounts, or none | Don't guess. Use your normal account checks. |

Before allowing sensitive actions, you can also require specific checks with `claims.attests(key)`, such as:

* `claims.attests("watchlist_clear")` for a clear watchlist result
* `claims.attests("phone_possession")` for a confirmed phone

## Next steps

* [Handling Failures](https://docs.baselayer.com/docs/kya-handling-failures.md): know why a credential was refused and what to tell the agent.
* [Business Agents](https://docs.baselayer.com/docs/kya-business-agents.md): check an agent acting for a business.


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