Skip to content

kyc: support individual and business entity types#354

Closed
larseidsvoll wants to merge 2 commits into
KeetaNetwork:mainfrom
larseidsvoll:feat/kyc-entity-types
Closed

kyc: support individual and business entity types#354
larseidsvoll wants to merge 2 commits into
KeetaNetwork:mainfrom
larseidsvoll:feat/kyc-entity-types

Conversation

@larseidsvoll

@larseidsvoll larseidsvoll commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Built in collaboration with @schenkty as part of project Gildor.

What this does

Lets a kyc provider declare which entity types it can verify (individual, business, or both) in its service metadata, and lets a verification request declare which type it is for. This folds business verification (KYB) into the existing kyc service rather than standing up a separate service.

How it works

Both individual and business are redirect flows: the provider returns a webURL to a hosted experience and the client polls for the certificate. The entityType tells the provider which hosted experience to present.

The provider hosts and owns the collection experience for both entity types (the demo-kyc-provider pattern: a hosted form served alongside the anchor API). So the request does not carry entity-specific input details, only which kind of experience to start. KYB detail collection lives in the anchor's hosted form, not in the SDK.

This is intentionally a minimal surface. An earlier draft carried a business-details block in the request and made webURL optional for a synchronous business path; that was dropped once we settled on the anchor hosting the KYB form (so business redirects too, and webURL stays required for both).

Changes

  • common.ts: add entityType to the request (defaults to individual). webURL stays required. KYCEntityType is derived from the metadata type for one source of truth.
  • server.ts: add entityTypes to the kyc config (defaults to ['individual']) and publish it in the service metadata.
  • client.ts: pass entityType through to the resolver lookup.
  • resolver.ts: add entityTypes to the kyc service metadata; add an optional entityType filter to the kyc search criteria; filter providers by it in lookupKYCServices. A provider with no declared entityTypes is treated as individual-only.
  • server.test.ts: business entity test covering metadata advertisement, entityType-filtered resolution, and a business createVerification that returns a hosted webURL.

The cert schema needs no KYB changes

The existing KYC certificate attribute set (generated from oids.json) is already generic and entity-agnostic, so KYB needs nothing added:

  • EntityType already has an organization arm (SEQUENCE OF GenericOrganizationIdentification) alongside person.
  • OrganizationIdentification already carries bic, lei, and a generic other ({ id, schemeName, issuer }).
  • Document is a single generic container (number, front/back/selfie references, dates, issuing authority) that every document type already reuses.

Business identifiers (EIN, registration number, LEI, DUNS) map to entityType.organization[] via ISO20022 scheme names, and KYB documents map to the generic Document. No per-document cert fields are required.

Back-compat

entityType defaults to individual and entityTypes defaults to ['individual'], so existing providers and callers are unaffected.

Verification

  • npx tsc --noEmit: 0 errors
  • make do-lint: clean
  • make test: the only failures are the pre-existing common.test.ts error round-trip flake, which fails identically on a clean main checkout (2 failed / 549 passed there too). The changed files (server, client, resolver tests) all pass.

Comment thread src/lib/resolver.ts Outdated
Comment on lines +105 to +119
/**
* The entity types which this KYC provider can
* verify. If not specified, the provider is
* assumed to verify `individual` entities only,
* preserving the classic KYC behavior.
*
* - `individual` is the classic KYC redirect flow
* (the provider returns a `webURL` hosted journey).
* - `business` is a Know Your Business (KYB) flow,
* performed synchronously from supplied business
* details with no `webURL`.
*
* A provider that supports both lists both values.
*/
entityTypes?: ('individual' | 'business')[];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an object so we can enforce keys are not added multiple times

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e9809ca. entityTypes is now a presence map { individual?: true; business?: true } so each type can be declared at most once. The resolver reads it via ('object') and matches on Object.keys(...), and the server publishes it by folding the author-facing array into the map.

Comment thread src/lib/resolver.ts Outdated
* entity type (a provider with no declared `entityTypes` is
* treated as `individual`-only).
*/
entityType?: 'individual' | 'business';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably have a type associated with it as it is re used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e9809ca. Extracted a named KYCEntityType ('individual' | 'business') exported from resolver and reused across the metadata field, this search criteria, common.ts, and server.ts.

Comment thread src/services/kyc/server.ts Outdated
Comment on lines +267 to +272
/*
* The hosted-journey webURL only applies to individual
* verifications. A business (KYB) verification is performed
* synchronously from the supplied business details, so there
* is no user-facing redirect and no webURL is required.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not have to be the case, the anchor should allow hosted journeys for businesses aswell

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed. Business is now a hosted redirect flow just like individual: the provider returns a webURL to its own hosted experience and the entity type only selects which form to present. The earlier synchronous/no-webURL branch is gone, and webURL is required for both.

Comment thread src/services/kyc/server.test.ts Outdated
Comment on lines +231 to +247
const businessResponse = await fetch(createURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
request: {
countryCodes: ['US'],
account: requesterAccount.publicKeyString.get(),
signed: signed,
entityType: 'business',
business: {
names: ['Acme, Inc.'],
addresses: [{ streetAddress1: '1 Main St', city: 'Springfield', state: 'IL', postalCode: '62701' }],
persons: [{ firstName: 'John', lastName: 'Doe' }]
}
}
})
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the client test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved in e9809ca. The business createVerification now runs through the client in client.test.ts (resolves a business-capable provider, calls startVerification, asserts a webURL). The server test keeps just the metadata-publish and resolver-lookup coverage.

Comment thread src/services/kyc/common.ts Outdated
Comment on lines +136 to +141
*
* Present for `individual` verifications (the hosted journey). Absent
* for `business` verifications, which are performed synchronously from
* the supplied business details with no user-facing redirect.
*/
webURL: string;
webURL?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This comment is wrong, the anchor should allow webURL for both. If anything, we probably want to enforce the web url flow for everything as every provider we use gives us a web flow. If there is not a web flow, there is a manual flow. We should try to avoid sending documents through this service as then we need to maintain what each provider might want to request.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly the direction we took. The business-details block and its document/identifier fields are removed from the request, webURL is required for both flows, and the provider hosts and owns collection for both. The package only carries which entity type to start, so we never have to model what each provider wants to request.

Let a kyc provider declare which entity types it can verify (individual,
business, or both) and let a verification request declare which type it is
for. Both are redirect flows: the provider returns a webURL to a hosted
experience and the client polls for the certificate. The entity type tells
the provider which hosted experience to present.

The provider hosts and owns the collection experience for both entity types
(see the demo-kyc-provider app pattern: a hosted form served alongside the
anchor API). So the package does not carry entity-specific input details in
the request -- it only carries which kind of experience to start. This keeps
the surface minimal and leaves KYB detail collection to the anchor's hosted
form rather than the SDK.

Changes:
- common.ts: add entityType to the request (defaults to individual). webURL
  stays required (both flows redirect). KYCEntityType is derived from the
  metadata type.
- server.ts: add entityTypes to the kyc config (default ['individual']) and
  publish it in the service metadata.
- client.ts: pass entityType through to the resolver lookup.
- resolver.ts: add entityTypes to the kyc service metadata, add an optional
  entityType filter to the kyc search criteria, and filter providers by it
  in lookupKYCServices (a provider with no declared entityTypes is treated
  as individual-only).
- server.test.ts: business entity test covering metadata advertisement,
  entityType-filtered resolution, and a business createVerification that
  returns a hosted webURL.

The cert schema needs no change for KYB: the existing ISO20022 attribute set
(EntityType.organization, OrganizationIdentification bic/lei/other, the
generic Document container) already represents business identifiers and
documents.

Back-compat: entityType defaults to individual and entityTypes defaults to
['individual'], so existing providers and callers are unaffected.

tsc clean, make do-lint clean. (Pre-existing common.test.ts error round-trip
flake fails identically on clean main -- not introduced here.)
@larseidsvoll larseidsvoll force-pushed the feat/kyc-entity-types branch from b43eeb2 to 8be46e8 Compare June 5, 2026 01:11
- entityTypes metadata is now a presence map ({ individual?: true,
  business?: true }) so a type cannot be declared twice, per review
- extract a named KYCEntityType type in resolver and reuse it across
  the metadata, search criteria, common, and server modules
- move the business createVerification assertion into the client test
  where the rest of the client flow lives; server test keeps the
  metadata-publish and resolver-lookup coverage
@larseidsvoll

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed e9809ca addressing all five comments:

  1. entityTypes is now an object (presence map { individual?: true; business?: true }) so a type cannot be declared twice.
  2. Named KYCEntityType type extracted in resolver and reused across metadata, search criteria, common, and server.
  3. Business createVerification moved to the client test; the server test keeps metadata-publish and resolver-lookup coverage.
  4. Business is a hosted redirect flow like individual now. The synchronous/no-webURL branch is gone and webURL is required for both. The provider hosts its own business-details form (the demo-kyc-provider pattern).
  5. No documents through this service. The business-details block and its fields are removed from the request. The package only carries which entity type to start, so we never model what each provider wants to collect.

tsc and lint clean. The only make test failures are the pre-existing common.test.ts error round-trip flake, which fails identically on a clean main checkout.

@larseidsvoll

Copy link
Copy Markdown
Contributor Author

Superseded by #355, opened from a branch on this repo (KeetaNetwork/anchor) instead of my fork so CI runs with repository secrets. Same commits, all the review comments here are already addressed there. Closing this and the fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants