Skip to main content

Ownership discovery

A KYB step can discover a company's beneficial owners automatically, instead of asking the applicant to declare them. When it does, Zyphe buys the ownership graph from official business registers as soon as the applicant submits their business information, traverses it to natural persons, and materializes the discovered owners and directors onto the KYB result, where they are screened like any declared person.

The conceptual model — how the graph is traversed, how effective ownership is multiplied along a chain, and what happens when no natural person crosses the 25% threshold — is described in Ownership structure and recursive UBO discovery. This page is how you turn it on and operate it.

Turning it on

Discovery is configured on the anchor KYB step:

FieldTypeDefaultMeaning
discoveryEnabledbooleanfalseDispatch ownership discovery when business information is submitted.
discoveryBudgetCreditsintegerPer-verification cap on discovery spend, in credits. Omit for the platform default.
Breaking change: these fields moved

discoveryEnabled and discoveryBudgetCredits used to live on the KYB_UBOS satellite's configuration. They now live on the anchor, because discovery is dispatched by the anchor's business-information submission and the opt-in has to sit on the step that triggers it. Existing flows were migrated in place — there is nothing to do unless you assemble flow configuration yourself, in which case move the two keys.

The default is false. A traversal spends your credits, so a flow that never opted in never dispatches one.

Discovery also has to be available

Beyond the step flag, ownership discovery has to be switched on for your deployment and for the country being verified. Ask Zyphe support to confirm coverage before you rely on it in production, and keep the declaration fallback in mind for the countries it does not reach.

What the applicant sees

Enabling discovery is the decision to let it stand in for the declaration steps. It is a single flag, not a pair of independent switches.

With discoveryEnabled on, the applicant submits business information and then never walks the KYB_UBOS or KYB_DIRECTORS steps at all. Those satellites stay hidden. Discovered people are written onto the result and their KYC invitations and screenings proceed exactly as they would for declared people.

The applicant's session can read the discovery's progress and result:

GET /flow/{flow_id}/vr/{verification_request_id}/kyb/discovery

It returns statusNONE when no traversal has been dispatched, otherwise PENDING, COMPLETED, or FAILED for the base run — plus ownershipStructure, uboSuggestions, and optional stoppedReason and errorMessage. It deliberately does not return the expansion history: per-node spend is your organization's business, not the applicant's. Read that from the organization-scoped endpoint below.

Discovered owners arrive without an email address, so they are screened for PEP and sanctions by name. They are not invited into a KYC flow.

When discovery cannot finish

Registers do not cover every jurisdiction, and a chain can dead-end at an entity nothing authoritative reports. When discovery fails or produces nothing usable, Zyphe falls back to asking the applicant — it raises a declaration document request that reopens the hidden satellite for a fresh declaration:

Request typeReopens
UBO_DECLARATIONKYB_UBOS
DIRECTOR_DECLARATIONKYB_DIRECTORS

A declaration request is raised automatically when the traversal fails, stops early, produces no natural-person owner (or, for directors, no director), or is still unfinished after the fallback window. The applicant receives an email with a deep link back into the reopened step, and answers it by submitting that part normally — the request flips to UPLOADED on submission.

While one is outstanding, the KYB cannot be approved — not by an agent, not by the automatic completion path, and not by a human reviewer.

Each part is asked at most once, ever. If the applicant's declaration is unsatisfactory, take it forward as an ordinary clarification or document request rather than expecting a second automatic reopen. A reviewer can also raise one manually:

POST /organizations/{organization_id}/kyb/{kyb_result_id}/document-requests
{ "requests": [{ "requestType": "UBO_DECLARATION", "description": "Please declare the beneficial owners." }] }

Raising a declaration request on a step that does not have discoveryEnabled returns a 400 — that flow already collects declarations directly, so there is nothing to reopen.

requestType on document requests

Every item in a document-request creation payload carries a requestType. It now has four values: STANDARD, CLARIFICATION, and the two new UBO_DECLARATION and DIRECTOR_DECLARATION.

Enrichment: the ENRICHING status

Between submission and review, a KYB result sits in ENRICHING while Zyphe buys the authoritative registry record the business-information cross-check is graded against.

ENRICHING is an API read-only status:

  • It appears when you read a KYB result through the API or see it in the dashboard.
  • Webhooks report it as PENDING_MODERATION, so existing integrations are unaffected and no webhook consumer needs to learn a new value.
  • No endpoint accepts it as a target status. Approve, moderate, and manual-review all reject a result that is still in it.
  • A timeout sweep guarantees nothing stays in it indefinitely.

The full status vocabulary is in Statuses and codes.

Expanding the graph node by node

The automatic traversal runs against a budget. Once it is spent, the graph can be truncated: branches that were not followed to their end are recorded as such rather than silently dropped. A reviewer can then pay to continue specific branches, one node at a time, from the KYB result page.

These endpoints are organization-scoped and available to your organization administrators.

Preview what an expansion would cost

GET /organizations/{organization_id}/kyb/{kyb_result_id}/expansion-preview

A pure read: no register is queried and nothing is charged. It answers "which nodes can I expand, and what would each cost?"

{
"candidates": [
{
"nodeId": "n_7",
"legalName": "Beta Holdings S.à r.l.",
"countryCode": "LU",
"nodeStatus": "budget_truncated",
"expandable": true,
"alreadyExpanded": false,
"estimatedCredits": 42,
"estimateBasis": "variableCap",
"expectedLatency": "slow"
},
{
"nodeId": "n_9",
"legalName": "Gamma Ltd",
"countryCode": "KY",
"nodeStatus": "coverage_unavailable",
"expandable": false,
"reason": "countryUnsupported"
}
],
"inFlight": [{ "nodeId": "n_4", "discoveryId": "…", "startedAt": "2026-08-30T02:00:00Z" }],
"concurrencySlotsFree": 3,
"pricingFetchedAt": "2026-08-30T02:00:00Z"
}
FieldMeaning
countryCodeThe node's own country. Pricing and coverage are decided by it, not by the subject company's country — expanding a Cayman parent of an Italian subject prices against Cayman.
expandableWhether the node can be sent to extend-discovery right now.
alreadyExpandedReported, not a rejection. Re-pulling a node is legitimate and is usually near-free thanks to source-side deduplication.
estimatedCreditsEstimated cost in credits. Absent when expandable is false.
estimateBasisfixedPrice (a firm per-country price), variableCap (a ceiling the request is capped at, never a firm price), or unknown (a guess against the same conservative ceiling).
expectedLatencyfast (seconds), slow (minutes), or manual (days, for jurisdictions where the record is retrieved by hand).
reasonWhy a node is not expandable — see the table below.
concurrencySlotsFreeHow many more expansions this KYB result can have in flight at once.

Optional fields are omitted rather than serialized as null. Treat "key absent" and "key null" identically.

The preview is only meaningful once the base traversal has completed — before that there is nothing to expand, and a dispatch attempt rejects every node with baseRunInFlight.

Dispatch the expansion

POST /organizations/{organization_id}/kyb/{kyb_result_id}/extend-discovery
{ "nodeIds": ["n_7", "n_9", "n_4"], "maxCreditsPerNode": 60 }
FieldRequiredMeaning
nodeIdsyes1–25 company node ids from the stored ownership graph.
maxCreditsPerNodenoPer-node ceiling, in the credits you are actually charged. It only ever binds downward: on a variable-priced country it replaces the platform ceiling; on a fixed-price country it cannot raise the price, but a cap below that price makes the node come back rejected with the real cost rather than overspending it.
Breaking change: the response shape

The response is now a per-node dispatch report (202), not a single {discoveryId, status} job handle. One register request, one row, and one verdict per requested node, with partial acceptance. The old top-level discoveryId and status fields are gone — with N rows they were never meaningful.

{
"verificationRequestId": "…",
"estimatedCreditsTotal": 84,
"expansions": [
{ "nodeId": "n_7", "accepted": true, "discoveryId": "…", "status": "PENDING", "estimatedCredits": 42, "estimateBasis": "variableCap" },
{
"nodeId": "n_4",
"accepted": true,
"discoveryId": "…",
"status": "COMPLETED",
"estimatedCredits": 42,
"estimateBasis": "variableCap",
"chargedCredits": 17
},
{ "nodeId": "n_9", "accepted": false, "reason": "countryUnsupported" }
]
}

A node can finalize inline when the source responds synchronously, in which case it comes back COMPLETED with chargedCredits. Otherwise the charge arrives later. verificationRequestId in the response is the anchor's verification request: join its websocket room to receive discovery-update events carrying discoveryId, rootNodeId, status, and chargedCredits as each node settles, or poll the discovery read.

The endpoint is rate limited to 10 calls per minute per KYB result, and a single result can have a limited number of expansions in flight at once — concurrencySlotsFree on the preview tells you how many are left.

Partial acceptance replaces all-or-nothing. An unknown or non-expandable id is reported in place in expansions[]. Only a request where every node is rejected returns 400, carrying each node's reason.

Rejection reasons. expansion-preview can return the first seven; extend-discovery can return all ten.

ReasonMeaning
notACompanyNodeThe node is a natural person, not a company.
countryUnsupportedNo source covers that node's country.
graphDatapointUnavailableOwnership data is not available for that country.
expansionInFlightAnother request is already expanding this node.
baseRunInFlightThe base traversal has not finished yet. Every node is rejected with this.
concurrencyCapThis KYB result already has the maximum number of expansions in flight.
unknownNodeIdNo such node in the stored graph.
providerErrorDispatch-time only. The lookup or row creation failed.
budgetExceededDispatch-time only. The continuation was refused as over budget. Free, and may carry a re-quote.
insufficientCreditsDispatch-time only. Your balance no longer covers this node once other in-flight traversals' commitments are counted.

Clients that match exhaustively on reason must handle all ten.

402 Payment Required means the summed estimate of every accepted node, plus what this KYB's in-flight traversals have already committed, exceeded your credit headroom. It is deliberately all-or-nothing: zero rows were created and zero lookups were made, so there is nothing to reconcile and nothing to cancel. Top up, lower maxCreditsPerNode, or ask for fewer nodes, and retry.

Cancel an expansion

DELETE /organizations/{organization_id}/kyb/{kyb_result_id}/extend-discovery/{discovery_id}

Returns 204 No Content, or 409 if the expansion has already finished or failed.

Cancelling means "stop waiting for this and let me move on", not "stop spending". The traversal continues at the source, which exposes no abort, and you are billed for the work it does. When it eventually settles, the result is kept rather than discarded: the graph is merged into the KYB result, discovered people are materialized, and the real final cost is billed once. Paying for a traversal and throwing its result away would be the worse outcome. Your cancellation note is preserved in the audit trail either way.

Cancelling releases the node's lock immediately, so a fresh expansion of the same node can be dispatched right away rather than waiting out a timeout.

Reading the full picture

GET /organizations/{organization_id}/kyb/{kyb_result_id}/discovery

The reviewer's full read: the base traversal's status, the discovered graph, the derived UBO suggestions, and the per-node expansion history with its per-node spend. A pure read — no lookup, no charge.

{
"status": "COMPLETED",
"ownershipStructure": {},
"uboSuggestions": [],
"expansions": [
{
"discoveryId": "…",
"rootNodeId": "n_4",
"status": "COMPLETED",
"chargedCredits": 17,
"stoppedReason": null,
"createdAt": "2026-08-30T02:00:00Z",
"completedAt": "2026-08-30T02:01:00Z"
}
]
}

chargedCredits is always present and is 0 before the charge is claimed. 0 is not the same as "free" — read status to tell a not-yet-charged row from a genuinely zero-cost one (a deduplicated lookup). stoppedReason, when present, is one of completed, budget_exhausted, max_depth_reached, max_nodes_reached, or timeout.

What a KYB costs

Everything below is denominated in credits, deducted from your organization's balance. See Billing for how credits are funded.

Every KYB carries a base charge, resolved for the jurisdiction of the business being verified, falling back to your plan's KYB credit cost when no jurisdiction price applies. Zyphe can set a per-organization override of a jurisdiction's price for you.

What that base charge covers depends on whether discovery is on:

discoveryEnabledCharging modelWhat you pay
false (default)FlatThe jurisdiction base charge, and nothing else. The registry lookup behind the business-information cross-check is included — it is performed and costs you no extra credits.
trueMeteredThe jurisdiction base charge, plus the ownership-graph traversal and any operator-initiated node expansions, each charged in credits.

Flat versus metered is a per-KYB property derived from the anchor step's discoveryEnabled, not an organization-level setting. Two flows in the same organization can charge differently.

Within a metered KYB:

  • The automatic traversal is bounded by discoveryBudgetCredits on the step.
  • Each node expansion is priced and charged individually, against the node's own country, and optionally capped by maxCreditsPerNode on the request.
  • Sandbox draws the same credit balance as production. There is no separate sandbox balance.

Approval preconditions

Approval readiness is now enforced identically everywhere — the organization approve endpoint, the platform moderation endpoint, the manual-review endpoint, and the agent's approve tool. All of them return a 400 naming the reason:

SituationWhat you get
The result is still ACQUIRING or ENRICHING400 — the result is still being prepared and is not yet eligible for review.
Ownership discovery is still running400 — ownership discovery outstanding.
A UBO or director declaration request is unanswered400 — the applicant still owes a declaration.
Required documents missing, business information incomplete, or a person's KYC or screening unfinished400 naming the specific gap.

This is stricter than before: paths that previously let an approval through in these states now refuse it.