# The Collect SDK
Source: https://docs.solo.one/api-overview/collect-sdk/overview
A hosted, drop-in flow for collecting first-party identity data and consent
**Available to registered partners only.** The Collect SDK is provisioned per
partner. Contact your SOLO representative to request access and a sandbox.
## What it is
The **Collect SDK** is a ready-made screen flow that asks a person for their
information and permission, so you don't have to build those screens yourself.
You hand the consumer off to it, they answer a few guided steps — who they are,
a quick phone check, what they agree to share — and the result comes back to you
through the SOLO network.
Think of it as the "front door" where data and
[consent](/concepts/identity/consent) are gathered directly from the consumer.
It's the human-facing counterpart to SOLO's machine interfaces: the
[Furnishing API](/api-overview/furnishing/overview) (how data gets *into* a network)
and the [Query API](/api-overview/querying/overview) (how data is read back).
## Why it's useful
* **You skip building intake UI.** Identity capture, phone one-time-passcode
verification, consent screens, dynamic forms, and conflict resolution are all
provided and kept current by SOLO.
* **Consent is captured at the source.** The consumer's
[consent](/concepts/identity/consent) is recorded in the same flow that
collects their data, so the legal basis travels with the data from the moment
it's gathered.
* **You can change what's collected without a release.** The steps and fields
are decided on the server, so product and compliance changes don't require
shipping new front-end code.
## When and why you'd use it
Reach for the Collect SDK whenever you need a real person to provide information
or grant permission as part of a flow you run:
| Situation | How the Collect SDK helps |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Onboarding a new customer** | Walks them through identity, phone verification, and consent in one guided flow, then makes the result available to the network. |
| **Getting permission to query someone** | Captures the consumer's [consent](/concepts/identity/consent) so you can later run a [product query](/api-overview/querying/overview) about them. |
| **Collecting missing details** | Presents a dynamic form for exactly the fields you still need — without you designing the form. |
| **You don't want to build or maintain intake screens** | SOLO hosts and updates the experience; you just launch it. |
If your data already lives in your own systems and no consumer interaction is
needed, you'll typically use the [Furnishing API](/api-overview/furnishing/overview)
instead. The Collect SDK is specifically for the moments a consumer is in the
loop.
## How it works
The Collect SDK is a **hosted experience**, not a code library you bundle. You
integrate by minting a short-lived session token from your backend and opening
the hosted SDK URL — there's no client package to install or upgrade.
```mermaid theme={null}
flowchart LR
App[Your application] -->|mint session token| SDK[Collect SDK
hosted flow]
SDK -->|guided steps| User((Consumer))
User --> SDK
SDK -->|consent + collected data| Net[(SOLO network)]
```
Your backend requests a short-lived SDK session token, scoped to the
workflow you want the consumer to complete (and, optionally, the
[entity](/concepts/identity/entities) it concerns). Tokens are
single-purpose and time-limited.
Send the consumer to the hosted Collect SDK URL with the token. The SDK
validates the token and renders the first step. No token, no flow — the SDK
will not start without a valid session.
The SDK drives a step-by-step flow (below), advancing as each step is
submitted. The sequence and the fields requested are decided server-side by
the workflow.
On completion the consumer returns to your application, and the consented,
collected data is available in the network for furnishing or querying.
### The collection flow
A Collect session is a sequence of server-driven steps. A given workflow uses
the subset it needs:
| Step | What the consumer does |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Identity** | Provides core identifying details (e.g. name and phone number). |
| **Phone verification** | Confirms ownership of their phone with a one-time passcode. |
| **Consent** | Affirms or denies consent per attribute, for exactly the attributes the workflow requests. See [Consent](/concepts/identity/consent). |
| **Collect** | Fills in a dynamic form — fields (email, address, dates, selections, and so on) are specified by the workflow, not hard-coded in the UI. |
| **Conflict resolution** | Chooses between conflicting values when the same attribute arrives from more than one source. |
| **Review** | Confirms the aggregated consents and collected values before submitting. |
Because the steps and fields are defined by the workflow on the server, the same
hosted SDK can power very different collection experiences — a light phone-and-
consent capture, or a full onboarding intake — without any change on your side.
## Related concepts
The permission the SDK captures alongside the data.
The consumer or business a Collect session concerns.
How collected data is contributed into a network.
How collected, furnished data is read back.
# Bulk File Upload
Source: https://docs.solo.one/api-overview/furnishing/file-upload
Ingest data files through the REST file-upload endpoint
The bulk file upload endpoint lets you furnish an entire file of records with a
single HTTPS request — no SFTP credentials, no file-transfer infrastructure.
You `POST` a CSV file plus a category slug; the network accepts it
synchronously, queues it for ingestion, and processes the rows asynchronously.
It sits between the other two [furnishing channels](/api-overview/furnishing/overview):
heavier than a per-record REST furnish, lighter than standing up a recurring
[SFTP](/api-overview/sftp/overview) pipeline. Reach for it when you have a batch
job that already speaks HTTPS and produces files on a schedule you control.
## The endpoint
```
POST /v1/file-upload/ingest
```
The request is `multipart/form-data` with two parts:
| Part | Type | Required | Description |
| ------ | ---------- | -------- | ----------------------------------------------------------------------------------------------- |
| `file` | file | Yes | The CSV file to ingest. The first row must be a header row; every subsequent row is one record. |
| `slug` | form field | Yes | The data category for this upload. Determines how the rows are interpreted. |
### Accepted categories
`slug` must be one of the five upload categories — the same set used by the
SFTP channel's directory layout:
| Slug | Contents |
| ------------------ | ---------------------------------- |
| `kyc_cert_policy` | KYC certificate policy definitions |
| `kyb_cert_policy` | KYB certificate policy definitions |
| `subnetworks` | Network subnetwork configuration |
| `kyc_furnish_data` | Consumer onboarding records |
| `kyb_furnish_data` | Business onboarding records |
Any other value is rejected with a `400` validation error. The expected
columns for each category are documented in the
[upload categories reference](/api-overview/sftp/schemas).
### Filename rules
The uploaded filename is validated before anything else:
* Exactly **one dot**, separating the name from the extension.
* The name may contain only **letters, digits, underscores, and hyphens**.
* The extension must be one of `csv`, `xls`, or `xlsx` (case-insensitive).
So `kyc-batch_2026-06.csv` is valid; `kyc batch.csv` (space),
`kyc.batch.csv` (two dots), and `batch.txt` (extension) are all rejected with
a `400` whose body lists every rule that was violated.
The request body is parsed as **CSV** — UTF-8 text with a header row (a
leading byte-order mark is tolerated). Send CSV content. Excel workbooks
belong on the [SFTP channel](/api-overview/sftp/overview), which parses `.xlsx`
natively.
## Example
A CSV with the category's column headers in the first row. For
`kyc_furnish_data`:
```csv theme={null}
file,social_security_number,date_of_birth,first_name,last_name,subnetwork_name,application_date
F-1001,123456789,1990-01-15,Jane,Doe,default,2026-05-28
F-1002,987654321,1985-09-02,John,Smith,default,2026-05-29
```
```bash theme={null}
curl -X POST https://api.solo.one/v1/file-upload/ingest \
-H "Authorization: Bearer $SOLO_TOKEN" \
-F "file=@kyc-batch_2026-06.csv" \
-F "slug=kyc_furnish_data"
```
The endpoint responds as soon as the file is received and queued. The body
is the created upload record:
```json theme={null}
{
"id": "7c3f2a91-4b8e-4d06-a2c5-91e84f0b6d23",
"entity_id": "2d6b8e44-9f01-4c7a-8a3e-5b1c0d92f761",
"account_id": "a1f09c35-6e72-4b88-b4d1-3c8e7a52d910",
"status": "pending",
"slug": "kyc_furnish_data",
"source": "inline_upload",
"original_filename": "kyc-batch_2026-06.csv",
"file_format": "csv",
"content_type": "text/csv",
"declared_byte_size": 2048
}
```
Keep the `id` — it identifies this upload in the dashboard and in any
follow-up with support.
## Upload lifecycle
The upload is accepted synchronously and processed asynchronously. The
record's `status` tracks where it is:
```mermaid theme={null}
stateDiagram-v2
[*] --> pending : upload accepted
pending --> processing : worker picks it up
processing --> completed : all rows handled
processing --> failed : ingestion error
```
| Status | Meaning |
| ------------ | --------------------------------------------------------------------------------- |
| `pending` | Accepted and queued; not yet picked up. This is what the `POST` response returns. |
| `processing` | A worker is ingesting the rows. |
| `completed` | Ingestion finished. |
| `failed` | Ingestion hit an unrecoverable error. |
Small files typically move from `pending` to `completed` within seconds. The
SOLO dashboard's uploads view shows the current status of every file your
organization has sent.
### Retries and idempotency
Every request to the endpoint creates a **new upload record** with its own
`id` — retrying a failed `POST` is always safe and never half-applies a file.
What happens to the *rows* on a re-send depends on the category:
* **Data rows** upsert on their natural keys (SSN for consumers; tax
identifier + jurisdiction for businesses). Re-sending a corrected batch
updates the affected records rather than duplicating them.
* **Configuration rows** (policies, subnetworks) are keyed by name. Re-sending
a file whose rows collide with existing names produces per-row name-conflict
errors; the originals are left untouched.
Because each row is processed independently, a partially bad file partially
succeeds: fix the failed rows and re-send just those (or the whole file, given
the upsert behavior above).
## How uploads connect to subnetworks and policies
The `slug` decides what the rows *are*; the rows themselves decide where the
data *goes*:
* **Data uploads** (`kyc_furnish_data`, `kyb_furnish_data`) carry a
`subnetwork_name` and `application_date` on every row. At ingest these drive
the same subnetwork routing and
[furnishing-policy resolution](/api-overview/furnishing/overview#subnetwork-routing-and-policy-resolution)
as a REST furnish — each row is matched to the subnetwork's linked policies
by application date, and rows outside every policy window are filtered rather
than errored.
* **Configuration uploads** (`kyc_cert_policy`, `kyb_cert_policy`,
`subnetworks`) don't furnish entity data at all. They create the policies
and subnetworks that data uploads later resolve against, and are restricted
to the network's governor. Upload policies before subnetworks that reference
them, and subnetworks before data that targets them.
## Failure modes
One or more filename rules failed. The error body lists each violation
(extra dots, disallowed characters, bad extension). Rename the file —
letters, digits, `_`, `-`, one dot, `csv`/`xls`/`xlsx` — and retry.
The `slug` form field isn't one of the five allowed categories. The error
message includes the allowed set. Check for typos and stray whitespace —
the value is matched against the allowlist exactly (after trimming).
The endpoint requires a valid bearer token; the upload is attributed to
the authenticated organization. See
[Authentication](/home/authentication).
A `failed` status (or rows that never appear) usually means the file's
content didn't match the category's expected columns. Compare your header
row against the [category schema](/api-overview/sftp/schemas) — header names
matter, column order doesn't. The dashboard's uploads view surfaces
row-level errors.
The body must decode as UTF-8 (a BOM is fine) and parse as CSV with a
header row. Exports from spreadsheet tools in other encodings (e.g.
Latin-1) should be re-saved as UTF-8 CSV.
## In the dashboard
## When to use a different channel
* **Furnishing one record at a time, in real time** — use the product furnish
endpoints, e.g. `POST /v1/products/kyc_certificate/furnish`. See the
[furnishing overview](/api-overview/furnishing/overview).
* **Recurring drops from systems that produce files natively** — use
[SFTP](/api-overview/sftp/getting-started). Same categories, same ingestion
pipeline, but `.xlsx` workbooks and no HTTP client required.
Column-by-column reference for every category.
Full request/response schema for the ingest endpoint.
# Network contribution
Source: https://docs.solo.one/api-overview/furnishing/overview
How contributing verified work to the network works — and what a contributor keeps
**Furnishing** is how a participant contributes verified work into a network.
It's the write half of the network: a verification your institution already
performed becomes a reusable [trust asset](/concepts/trust/trust-assets) that
entitled participants can later query. If querying is how you take value out of
a SOLO network, contributing is how you put value in — and how you earn the
right to take value out in the first place.
## Contributing is asset creation, not data donation
The natural fear of contributing is "I'm giving away proprietary customer
intelligence and losing control." A SOLO network is designed so that isn't what
happens. When you furnish a verification, it does not vanish into a vendor
database — it becomes an **attributed, governed asset** you still have a stake
in:
* **You keep attribution.** Every certificate sub-product your data backs
carries your `furnishing_entity_id` and `attestation_id`. The verification
travels with your name on it — see [Provenance](/concepts/trust/provenance).
* **You keep governance.** Contributed data is still gated by the subject's
[consent](/concepts/identity/consent), the network's
[querying policy](/concepts/governance/querying-policies), and each reader's
[entitlement](/concepts/governance/entitlement). Furnishing does not expose
your data to passive observers.
* **You gain read access.** Contributing is how you earn entitlement to read a
subject back later (see below).
## Why furnish?
Banks furnish for two reasons, one regulatory and one economic:
1. **It's how the network has anything to say.** Every certificate, attestation,
and screening result a querier receives traces back to a record some
furnisher contributed. A network where nobody furnishes is an empty
database with a governance layer.
2. **It earns you read access.** Furnishing an entity makes you
[entitled](/concepts/governance/entitlement) to that entity's data on future
queries. Contributing is one of the two ways to earn entitlement — the other
is a prior query. For most participants, furnishing the entities you already
onboard is by far the cheapest way to build broad read coverage across the
network.
This earn-back loop is deliberate. Networks stay healthy when read access is
proportional to contribution, so the [entitlement
model](/concepts/governance/entitlement) treats every furnish as a deposit
against future reads.
## The three furnishing channels
There are three ways to get records into a network. All three converge on the
same ingestion machinery — the differences are in transport, batch size, and
how much automation you want to build.
| | REST furnish | Bulk file upload | SFTP drops |
| ------------------------ | ---------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------- |
| **Endpoint / transport** | `POST /v1/products/{product}/furnish` | `POST /v1/file-upload/ingest` | SFTP server (`sftp.solo.one`) |
| **Payload** | JSON records in the request body | One file per request (multipart) | Excel workbooks dropped into category directories |
| **Batch size** | One to a few records per call | One file, many rows | One workbook, many rows; many workbooks per session |
| **Processing** | Synchronous — response reflects the furnish | Asynchronous — returns an upload record to track | Asynchronous — ingestion triggers on upload |
| **Best for** | Real-time furnishing from your onboarding flow | Programmatic batch jobs that already speak HTTPS | Recurring scheduled drops from core-banking or data-warehouse exports |
When to choose each:
* **REST furnish** when furnishing is an event in your system — a consumer
finishes onboarding, you furnish their record in the same workflow. Lowest
latency, immediate feedback, no file handling.
* **[Bulk file upload](/api-overview/furnishing/file-upload)** when you have a
periodic batch job and an HTTPS client but don't want to manage SFTP
credentials or a file-transfer pipeline.
* **[SFTP](/api-overview/sftp/overview)** when your data already leaves your
systems as files on a schedule. Most core-banking and data-warehouse stacks
can target an SFTP endpoint with zero custom code, which makes it the usual
choice for nightly or monthly drops.
All three channels feed the same furnishing pipeline. A record furnished over
REST and the same record uploaded in a workbook over SFTP end up as identical
data in the network — same validation, same policy resolution, same
entitlement effect.
## How a furnish works
A furnish brings together a **network**, a **subnetwork**, a resolved
**furnishing policy**, and a **consumer** (or business):
```mermaid theme={null}
flowchart LR
Rec[Your records] --> F[Product furnish]
N[Network + subnetwork] --> F
P[Furnishing policy] -. resolved .-> F
F --> M[Matched to entity]
M --> Store[(Network data)]
```
| Concept | Role in the furnish |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Product** | *What* you're contributing (e.g. a KYC certificate). Determines the endpoint and record schema. |
| **Network** | *Where* the data is contributed. You must be a [furnisher](/concepts/governance/network-roles) of it. |
| **Subnetwork** | *Which partition* inside the network the data belongs to. Subnetworks link to the policies that govern acceptance. |
| **Policy** | The [furnishing policy](/concepts/governance/furnishing-policies) the network resolves automatically from subnetwork + application date. |
| **Consumer / Business** | *Who* the data is about — matched from the identifiers in each record. |
### Anatomy of a REST furnish
You call the product's furnish endpoint with the network scope and one or more
records. For a KYC certificate:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/furnish \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-7a44-4f1e-9d2a-08b6f3a1c5d7",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
}'
```
A successful furnish returns a submission acknowledgment:
```json theme={null}
{
"success": true,
"submission_id": "5b2e9f10-3c7d-4e8a-b1f6-2d9c0a47e831"
}
```
Field by field:
* `network_id` — the network you're furnishing into. You must hold the
furnisher role there.
* `subnetwork_name` — names the subnetwork inside that network. Together with
`application_date`, this is what the network uses to resolve the applicable
furnishing policy.
* `application_date` — when the consumer applied. Used in policy resolution
(see below), not as a record timestamp.
* `records` — one or more consumer records. For KYC certificates each record
carries `first_name`, `last_name`, `date_of_birth`, and
`social_security_number` — all strings. The SSN is the matching key that
connects your record to a consumer [entity](/concepts/identity/entities).
The same shape applies to `POST /v1/products/kyb_certificate/furnish` for
business records.
### Subnetwork routing and policy resolution
You never attach a furnishing policy yourself. At ingest, the network resolves
it in two steps:
1. **Subnetwork lookup.** `subnetwork_name` is resolved to a subnetwork inside
the network identified by `network_id`. An unknown subnetwork name fails the
record — subnetworks must be configured by the network's governor before
furnishers can target them.
2. **Policy matching.** Each subnetwork is linked to one or more
[furnishing policies](/concepts/governance/furnishing-policies), each link
carrying an effective date window. Your record's `application_date` is
compared against the policy's active window and the subnetwork-link's
effective window. Policies whose windows cover the application date apply;
records that fall outside every window are *filtered* — skipped without
being treated as errors.
```mermaid theme={null}
flowchart TD
R[Record: subnetwork_name + application_date] --> PG[Resolve subnetwork in network]
PG -->|not found| ERR[Record fails]
PG --> PM[Match linked furnishing policies]
PM -->|application_date in window| RUN[Ingest under policy]
PM -->|outside all windows| FIL[Filtered — skipped, not an error]
```
This is why the policy is "optional" from your perspective: it's always
applied, but never something you pass in. As a furnisher you state *facts*
(subnetwork, date, record); the governor's configuration decides *rules*. If a
subnetwork links several policies whose windows overlap your application date,
the record is ingested under each matching policy.
## What happens after ingest
Once a record is accepted, the furnishing pipeline takes over:
1. **Validation.** The record is checked against the resolved policy's
requirements. Each record is processed independently — one bad record in a
batch doesn't fail its neighbors.
2. **Entity matching.** Identifiers in the record (SSN for consumers; tax
identifier and jurisdiction for businesses) are matched to an existing
[entity](/concepts/identity/entities), or a new entity profile is created.
Re-furnishing the same entity updates the existing profile rather than
creating a duplicate.
3. **Records become network data.** The furnished attributes are written as
that entity's data, attributed to your organization as furnisher.
4. **Certificates and attestations.** For certificate products, the furnished
operations (document capture, address verification, and so on — whatever
the policy requires) become the evidence behind KYC/KYB certificates that
queriers later receive. Where your organization has attested to the data,
the attestation is linked to the resulting certificates.
5. **Entitlement is recorded.** Your organization becomes
[entitled](/concepts/governance/entitlement) to this entity's data on
future queries.
## Data quality expectations
The network is only as good as what's furnished into it. A few expectations
hold across every channel:
* **Identifiers must be real and complete.** SSNs, EINs, and dates of birth
are matching keys. A typo doesn't just corrupt one record — it creates a
phantom entity or pollutes a real one.
* **Treat identifiers as text.** SSNs and tax identifiers are strings, not
integers. This matters in spreadsheets especially: a numeric cell silently
drops leading zeros.
* **`application_date` should be the genuine application date.** It drives
policy resolution; backdating or defaulting it to "today" routes records to
the wrong policy version.
* **Furnish what the policy asks for.** Policies enumerate required
verification operations. Records that don't satisfy them won't produce
certificates, even if they ingest cleanly.
* **Idempotency is on natural keys.** Re-submitting a consumer record with the
same SSN updates rather than duplicates. Don't "fix" a bad record by
furnishing a second one with a tweaked identifier.
## Furnishing vs. querying
| | Furnishing | Querying |
| ------------------------ | --------------------------- | ---------------------- |
| Direction | Write into the network | Read from the network |
| Needs consent? | No | Yes |
| Needs a policy you pass? | No (resolved automatically) | Yes (`network_policy`) |
| Earns entitlement? | Yes | Yes |
See [Querying](/api-overview/querying/overview) for the read half.
## Troubleshooting
Your organization must hold the furnisher role in the target network. See
[network roles](/concepts/governance/network-roles). Check that
`network_id` is the network where you were granted the role — roles don't
carry across networks.
`subnetwork_name` must exactly match a subnetwork configured in the network
by its governor. Subnetwork names are matched within the network identified
by `network_id` — confirm both halves. If the subnetwork genuinely doesn't
exist yet, the governor needs to create it (over the API or a
[subnetworks workbook](/api-overview/sftp/schemas#subnetworks)) before you
furnish against it.
Most often the record was **filtered**: its `application_date` fell outside
every linked policy's effective window. Filtered records are skipped
deliberately, not errored. Check the subnetwork's policy links and their date
windows against the application dates you're sending.
Records are validated independently, so a batch can partially succeed.
Common causes: missing required fields (all four KYC record fields are
required), malformed dates (use ISO 8601, e.g. `1990-01-15`), or empty
identifier strings.
Entity matching keys on identifiers (SSN; EIN + jurisdiction). If you see
duplicates, the identifiers differed between submissions — check for
formatting drift such as dropped leading zeros or embedded whitespace.
## In the dashboard
## Related concepts
Furnish whole files over REST with one request.
Recurring workbook drops, no HTTP client required.
The rules the network applies to what you contribute.
A hands-on walkthrough of your first furnish.
# The Index Microservice
Source: https://docs.solo.one/api-overview/index-microservice/overview
Connect your data warehouse by indexing its schema — without exposing row values
**Available to registered partners only.** The Index Microservice is an
emerging capability provisioned per partner. Contact your SOLO representative
to discuss access before designing an integration.
## What it is
The **Index Microservice** lets you take part in a network using the data you
*already have*, without sending that data to SOLO. Instead of exporting records,
you connect your data warehouse and SOLO looks only at its **structure** — the
table and column layout — to learn what kinds of data you hold. Your actual
records stay in your warehouse; SOLO never reads the values during indexing.
In short: you point SOLO at your warehouse, SOLO learns the *shape* of your data
and lines it up with the [SOLO schema](/products/overview), and from then on your
existing data can serve the network in place.
## Why it's useful
* **No data export.** The raw records never leave your systems, which sidesteps
the cost and the data-handling concerns of copying sensitive data to a vendor.
* **No row values exposed.** Indexing reads schema and metadata only — names,
types, and shape — not the contents of any row.
* **Use what you already maintain.** If your verification data already lives in
Databricks, Snowflake, BigQuery, or similar, you can contribute from it
directly rather than building and running an export pipeline.
## When and why you'd use it
| Situation | Why the Index Microservice fits |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| **You hold large existing datasets** | Connect once and map, instead of continuously pushing records to SOLO. |
| **Exporting data is a non-starter** | Your records stay put; only schema and metadata are indexed. |
| **You already run a warehouse** | Reuse the data and access controls you maintain today. |
| **You want low-maintenance participation** | After the one-time connect and mapping, there's no per-record or per-file submission to operate. |
If instead you want to push data as events or scheduled files, use the direct
[furnishing channels](/api-overview/furnishing/overview) (REST, bulk upload, or
SFTP). The Index Microservice is the option for *leaving the data where it is*.
## How it works
The Index Microservice is effectively a fourth way to get data into a network,
alongside the three direct [furnishing](/api-overview/furnishing/overview) channels.
The difference is *where the data lives*:
| | REST / Bulk / SFTP furnish | Index Microservice |
| ------------------ | ---------------------------------- | ------------------------------------------------ |
| **Data movement** | You push records to SOLO | Data stays in your warehouse |
| **What SOLO sees** | The records you send | Schema and metadata only |
| **Setup** | Per-record or per-file submission | One-time connect + attribute mapping |
| **Best for** | Event-driven or batch contribution | Large existing datasets you don't want to export |
```mermaid theme={null}
flowchart LR
WH[(Your data warehouse
Databricks · Snowflake · BigQuery)]
WH -->|schema + metadata only| IDX[Index Microservice]
IDX -->|map columns to SOLO schema| Map[Attribute mapping]
Map --> Net[(SOLO network)]
```
Authorize a connection scoped to schema and metadata reads only — no
row-value access.
SOLO indexes the structure of the connected source: tables, columns, and
types.
Map your indexed columns onto SOLO models and fields, so the network knows
how your data corresponds to its shared vocabulary.
Once mapped, your indexed source can back the network's products under the
same [governance](/concepts/governance/networks) — consent, policy, and
entitlement — as any other contribution.
### Governance still applies
Indexing a source does not bypass the network's controls. Everything served
through an indexed source remains gated by the subject's
[consent](/concepts/identity/consent), the network's
[querying policy](/concepts/governance/querying-policies), and each reader's
[entitlement](/concepts/governance/entitlement) — exactly as with directly
furnished data.
## Related concepts
The direct channels for contributing data.
The SOLO schema your columns map onto.
The rules that govern what an indexed source can serve.
Who can read what, regardless of where the data lives.
# Coverage Check
Source: https://docs.solo.one/api-overview/querying/coverage-check
Non-billable soft check of field coverage before running a query
The **coverage check** is a non-billable, read-only pre-flight for product
queries. Given a product, a subject, and a network + policy scope, it reports —
**per furnisher** — which of the policy's required fields are covered by data
already furnished to the network. It answers *"if I run this query now, can it
succeed?"* without issuing a certificate, recording a query event, or creating
a billable event.
```text theme={null}
POST /v1/products/check
```
One endpoint serves every product: pass the `product_id` of the product you
intend to query.
## Why it exists
A product query that resolves but cannot satisfy the policy returns
[`204 No Content`](/api-overview/querying/overview#200-vs-204-here-is-data-vs-nothing-usable)
— and that resolved query is still a billable event. The coverage check lets you
avoid predictable `204`s:
```mermaid theme={null}
sequenceDiagram
participant App as Your app
participant API as SOLO API
App->>API: POST /v1/products/check (non-billable)
API-->>App: per-furnisher coverage
alt coverage complete
App->>API: POST /v1/products/{product}/query (billable)
API-->>App: 200 + certificate
else coverage incomplete
App->>App: skip query / adjust UX / pick another policy
end
```
## Request
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/check \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "3e8a1d5f-…",
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
| Field | Type | Required | Purpose |
| ----------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product_id` | UUID | yes | The product to coverage-check. |
| `consent_id` | string | one of¹ | Consent token — the system resolves the subject's identity. |
| `consumer_id` | UUID | one of¹ | Direct consumer profile reference (for consumer-targeted products). |
| `business_id` | UUID | one of¹ | Direct business profile reference (for business-targeted products). |
| `network_ids` | UUID\[] | yes | Networks to check across. |
| `policy_id` | UUID | no | The [querying policy](/concepts/governance/querying-policies) whose requirements to check against. If omitted, each network's default policy is used. |
| `furnishing_entity_ids` | UUID\[] | no | Restrict the check to specific furnishers. When omitted, all furnishers are considered. |
¹ Identify the subject the same way you would on the query itself: a
`consent_id`, or the direct profile id matching the product's target
(`consumer_id` for consumer products, `business_id` for business products).
## Response
A `200 OK` with one entry per furnisher that has relevant data:
```json theme={null}
{
"product_id": "3e8a1d5f-…",
"consumer_id": "c2a4e8d0-…",
"business_id": null,
"entities": [
{
"furnishing_entity_id": "e1d2c3b4-…",
"furnishing_entity_name": "First Example Bank",
"network_id": "9f1c0c2e-…",
"complete": false,
"models": [
{
"model_name": "DocumentCaptureEvent",
"met_count": 2,
"total_count": 2,
"fields": [
{ "field_name": "is_document_captured", "met": true },
{ "field_name": "document_capture_timestamp", "met": true }
]
},
{
"model_name": "LivenessCheckEvent",
"met_count": 0,
"total_count": 1,
"fields": [
{ "field_name": "is_liveness_captured", "met": false }
]
}
]
}
]
}
```
| Field | Meaning |
| ------------------------------------ | --------------------------------------------------------------------------------------------- |
| `entities[]` | One row per furnisher whose data was evaluated. |
| `entities[].complete` | Whether this furnisher's data alone satisfies every checked requirement. |
| `entities[].models[]` | Per-model breakdown, keyed by the product's model names (see each product's field reference). |
| `models[].met_count` / `total_count` | How many of the model's checked fields are covered. |
| `models[].fields[]` | Field-level detail: each `field_name` and whether it's `met`. |
The `field_name` values trace directly to the product's field reference tables
— see the [KYC certificate](/api-overview/querying/kyc-certificate#field-reference)
and [KYB certificate](/api-overview/querying/kyb-certificate#field-reference) pages.
If the `product_id` cannot be resolved, the endpoint returns `404` with the
standard error envelope (`{"detail": "…", "error_code": "RESOURCE_NOT_FOUND"}`)
— see [Errors](/home/errors).
## When to use it
* **Pre-flight UX.** Before showing a "verify with SOLO" path during
onboarding, check whether the network can actually produce a certificate for
this subject — and degrade gracefully if not, rather than surfacing a dead
end.
* **Avoiding empty 204s.** A billable query that returns `204` tells you the
policy's requirements weren't met *after* the fact. The coverage check tells
you the same thing for free, before you spend the query.
* **Choosing a scope.** The per-furnisher, per-network breakdown shows *which*
furnisher's data is complete, so you can target `furnishing_entity_ids` or
pick the right network — or a different policy — before querying.
* **Diagnosing a 204 you already received.** Run the same scope through the
coverage check to see exactly which model's fields fell short.
## What it does not do
The coverage check is a *soft* check. It does **not**:
* issue a certificate or return any subject data — only coverage booleans and
counts;
* record a query event — there is no `X-Ref-Id` to quote, and nothing is added
to the billable audit trail;
* create a billable event;
* guarantee the subsequent query returns `200` — data can change between the
check and the query, and the final result is still shaped by
[entitlement](/concepts/governance/entitlement) and the
[policy](/concepts/governance/querying-policies) at query time.
## Related
The billable query the coverage check pre-flights.
The catalog of products you can check and query.
# KYB Certificate
Source: https://docs.solo.one/api-overview/querying/kyb-certificate
Business-verification certificate for business onboarding
The **KYB Certificate** is a reusable Know Your Business attestation that
packages UBO (ultimate beneficial owner), incorporation, and business-identity
evidence into a single certificate. Rather than each institution repeating
registry lookups, ownership tracing, and risk screening for the same business,
a query assembles the verification work already furnished by network
participants into one network-issued result.
| | |
| -------------- | -------------------------------------------------------------------------------------- |
| **Category** | Identity Verification |
| **Use case** | Customer Onboarding |
| **Subject** | Business |
| **Operations** | `POST /v1/products/kyb_certificate/query`, `POST /v1/products/kyb_certificate/furnish` |
## What's in the certificate
A KYB certificate consolidates three **sub-products**, each a block in the query
response with its own `assertions` (what was attested, and when) and `data`
(the supporting attributes):
| Sub-product | Response key | What it attests |
| -------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Business identity verification | `business_identity_verification` | The legal entity exists and is what it claims — registration, jurisdiction, tax ID validation, operational existence. |
| Ownership & control verification | `business_ownership_control_verification` | Beneficial owners, control persons, and authorized representatives were identified and evidenced. |
| Risk & compliance assessment | `business_risk_compliance_assessment` | Sanctions, adverse media, restricted-activity, and activity-risk screening were performed. |
Every populated sub-product carries the `furnishing_entity_id` of the
participant whose data backed it and the `attestation_id` of their attestation.
Shared business descriptors — DBA name, website, jurisdiction of formation,
registration identifier — are repeated in each sub-product's `data` block so
each block stands alone.
## Querying
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyb_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
The request follows the standard [query anatomy](/api-overview/querying/overview):
identify the subject with `consent_id` (or `business_id` for direct
permissible-purpose lookups), and scope the read with `network_ids` plus an
optional `policy_id` and `furnishing_entity_ids`.
A `200 OK` means a certificate was issued:
```json theme={null}
{
"certificate_id": "8d4b2f6a-…",
"query_event_id": "7b2d9c4e-…",
"business_id": "b9e1f7c3-…",
"result": {
"meta": {
"network_id": "9f1c0c2e-…",
"policy_id": "5e7d2a14-…"
},
"business_identity_verification": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"business_identity_verification_assertion": true,
"business_identity_verification_performed_timestamp": "2026-05-02T00:00:00Z"
},
"data": {
"business_formation_document_artifact": "articles_of_organization.pdf",
"business_dba_name": "Acme Coffee",
"business_website_url": "https://acme.example",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier": "7423918",
"business_identity_verification_sources": ["state_registry"],
"business_entity_type": "llc",
"business_registration_status": "active",
"business_operational_existence_status": "verified",
"business_tax_id_validation_result": "match",
"business_address_verification_result": "match",
"business_registered_address": null,
"business_operating_address": null
}
},
"business_ownership_control_verification": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"business_ownership_control_verification_assertion": true,
"business_ownership_control_verification_timestamp": "2026-05-03T00:00:00Z"
},
"data": {
"business_ownership_evidence_artifact": "cap_table.pdf",
"business_dba_name": "Acme Coffee",
"business_website_url": "https://acme.example",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier": "7423918",
"business_ownership_control_verification_overall_status": "complete",
"business_ownership_control_verification_overall_primary_source": "state_registry",
"business_authority_determination_basis": "operating_agreement",
"business_authorized_representatives_identified_count": 2,
"business_beneficial_owners_identified_count": 1,
"business_beneficial_ownership_determination_method": "ownership_percentage",
"business_beneficial_ownership_threshold_applied": "25_percent",
"business_control_authority_evidence_reviewed_type": "operating_agreement",
"business_control_determination_basis": "managing_member",
"business_control_persons_identified_count": 1,
"business_ownership_evidence_reviewed_type": "cap_table"
}
},
"business_risk_compliance_assessment": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"business_risk_compliance_verification_assertion": true,
"business_risk_compliance_timestamp": "2026-05-03T00:00:00Z"
},
"data": {
"business_dba_name": "Acme Coffee",
"business_website_url": "https://acme.example",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier": "7423918",
"business_primary_activity_classification_code": "722515",
"business_primary_activity_classification_system": "NAICS",
"business_activity_risk_level": "low",
"business_sanctions_screening_result": "clear",
"business_adverse_media_assessment_result": "none_found",
"business_compliance_screening_scope_applied": "standard",
"business_restricted_activity_assessment_result": "clear",
"business_risk_compliance_assessment_overall_primary_source": "third_party",
"business_risk_compliance_assessment_overall_status": "verified",
"business_risk_compliance_assessment_timestamp": "2026-05-03T00:00:00Z"
}
}
}
}
```
* `certificate_id` — the issued certificate, recording its as-of date,
attestation timestamp, and the network + policy pairs it was resolved under.
* `query_event_id` — the billable query event id, also returned in the
`X-Ref-Id` response header.
* `result` — the consolidated certificate. Sub-products without qualifying data
are `null`.
## How the certificate resolves per network
The query gathers furnished business events across every network in
`network_ids`, applies the querying policy's filters uniformly, and selects the
**oldest matching event** from any allowed network for each sub-product. The
result's `meta.network_id` is anchored to the **first** network in your request
— list your primary network first. Which fields you ultimately see is shaped by
the [querying policy](/concepts/governance/querying-policies) and your
[entitlement](/concepts/governance/entitlement).
## When you get a 204
The endpoint declares a `204 No Content` response: *no certificate could be
created — the available data did not satisfy the policy requirements*. You'll
receive a `204` when:
* a required sub-product (identity, ownership & control, or risk/compliance)
was never furnished for this business in the queried networks, or
* furnished events exist but fail the policy's filters (e.g. verification older
than the policy's freshness window), or
* the policy selected specific data fields that the resolved certificate could
not populate.
The body is empty; the `X-Ref-Id` header is still present and the query event
is still recorded. Run a [coverage check](/api-overview/querying/coverage-check)
first to anticipate `204`s without spending a billable query.
## Furnishing
Participants contribute KYB data with
`POST /v1/products/kyb_certificate/furnish`:
```json theme={null}
{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{
"business_legal_name": "Acme Coffee LLC",
"business_ein": "12-3456789"
}
]
}
```
The response is `{"success": true, "submission_id": "…"}`. Bulk contribution is
also available via [file upload](/api-overview/furnishing/file-upload) and
[SFTP](/api-overview/sftp/overview); see [Furnishing](/api-overview/furnishing/overview)
for the full model.
## Field reference
The KYB certificate's data dictionary, model by model. *Field* is the display
name, *API name* is the `field_name` used in policies and coverage checks, and
*Source model* is the underlying table the value is drawn from.
### Business
| Field | API name | Type | Source model |
| ------------------- | --------------------- | ------ | ------------ |
| Business Legal Name | `business_legal_name` | String | `business` |
### BusinessIdentityVerificationEvent
| Field | API name | Type | Source model |
| -------------------------------------------- | ---------------------------------------------- | ------- | -------------------------------------- |
| Attestation ID | `attestation_id` | UUID | `business_identity_verification_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Identity Verified | `is_identity_verified` | Boolean | `business_identity_verification_event` |
| Identity Verification Timestamp | `identity_verification_timestamp` | Date | `business_identity_verification_event` |
| Identity Verification Overall Status | `identity_verification_overall_status` | String | `business_identity_verification_event` |
| Identity Verification Overall Primary Source | `identity_verification_overall_primary_source` | String | `business_identity_verification_event` |
| Is Secretary Of State Match | `is_secretary_of_state_match` | Boolean | `business_identity_verification_event` |
| Operational Existence Status | `operational_existence_status` | String | `business_identity_verification_event` |
| Tax ID Validation Result | `tax_id_validation_result` | String | `business_identity_verification_event` |
| Tax Identifier Type | `tax_identifier_type` | String | `business_identity_verification_event` |
| Tax Identifier Value | `tax_identifier_value` | String | `business_identity_verification_event` |
| Address Verification Result | `address_verification_result` | String | `business_identity_verification_event` |
| DBA Name | `dba_name` | String | `business_identity_verification_event` |
| Entity Type | `entity_type` | String | `business_identity_verification_event` |
| Formation Document Artifact | `formation_document_artifact` | String | `business_identity_verification_event` |
| Jurisdiction Of Formation | `jurisdiction_of_formation` | String | `business_identity_verification_event` |
| Registration Identifier | `registration_identifier` | String | `business_identity_verification_event` |
| Registration Status | `registration_status` | String | `business_identity_verification_event` |
| Website URL | `website_url` | String | `business_identity_verification_event` |
### BusinessOwnershipControlVerificationEvent
| Field | API name | Type | Source model |
| ----------------------------------------------------- | ------------------------------------------------------- | ------- | ----------------------------------------------- |
| Attestation ID | `attestation_id` | UUID | `business_ownership_control_verification_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Ownership Control Verified | `is_ownership_control_verified` | Boolean | `business_ownership_control_verification_event` |
| Ownership Control Verification Timestamp | `ownership_control_verification_timestamp` | Date | `business_ownership_control_verification_event` |
| Ownership Control Verification Overall Status | `ownership_control_verification_overall_status` | String | `business_ownership_control_verification_event` |
| Ownership Control Verification Overall Primary Source | `ownership_control_verification_overall_primary_source` | String | `business_ownership_control_verification_event` |
| Authority Determination Basis | `authority_determination_basis` | String | `business_ownership_control_verification_event` |
| Authorized Representatives Identified Count | `authorized_representatives_identified_count` | Integer | `business_ownership_control_verification_event` |
| Beneficial Owners Identified Count | `beneficial_owners_identified_count` | Integer | `business_ownership_control_verification_event` |
| Beneficial Ownership Determination Method | `beneficial_ownership_determination_method` | String | `business_ownership_control_verification_event` |
| Beneficial Ownership Threshold Applied | `beneficial_ownership_threshold_applied` | String | `business_ownership_control_verification_event` |
| Control Authority Evidence Reviewed Type | `control_authority_evidence_reviewed_type` | String | `business_ownership_control_verification_event` |
| Control Determination Basis | `control_determination_basis` | String | `business_ownership_control_verification_event` |
| Control Persons Identified Count | `control_persons_identified_count` | Integer | `business_ownership_control_verification_event` |
| Is Personally Guaranteed | `is_personally_guaranteed` | Boolean | `business_ownership_control_verification_event` |
| Ownership Evidence Artifact | `ownership_evidence_artifact` | String | `business_ownership_control_verification_event` |
| Ownership Evidence Reviewed Type | `ownership_evidence_reviewed_type` | String | `business_ownership_control_verification_event` |
### BusinessRiskComplianceEvent
| Field | API name | Type | Source model |
| ------------------------------------------------- | --------------------------------------------------- | ------- | -------------------------------- |
| Attestation ID | `attestation_id` | UUID | `business_risk_compliance_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Risk Compliance Assessed | `is_risk_compliance_assessed` | Boolean | `business_risk_compliance_event` |
| Risk Compliance Assessment Timestamp | `risk_compliance_assessment_timestamp` | Date | `business_risk_compliance_event` |
| Risk Compliance Assessment Overall Status | `risk_compliance_assessment_overall_status` | String | `business_risk_compliance_event` |
| Risk Compliance Assessment Overall Primary Source | `risk_compliance_assessment_overall_primary_source` | String | `business_risk_compliance_event` |
| Activity Risk Level | `activity_risk_level` | String | `business_risk_compliance_event` |
| Sanctions Screening Result | `sanctions_screening_result` | String | `business_risk_compliance_event` |
| Adverse Media Assessment Result | `adverse_media_assessment_result` | String | `business_risk_compliance_event` |
| Compliance Screening Scope Applied | `compliance_screening_scope_applied` | String | `business_risk_compliance_event` |
| Restricted Activity Assessment Result | `restricted_activity_assessment_result` | String | `business_risk_compliance_event` |
| Address Verification Result | `address_verification_result` | String | `business_risk_compliance_event` |
| Is On AML Watchlist | `is_on_aml_watchlist` | Boolean | `business_risk_compliance_event` |
| Is On OFAC Watchlist | `is_on_ofac_watchlist` | Boolean | `business_risk_compliance_event` |
| Primary Activity Classification Code | `primary_activity_classification_code` | String | `business_risk_compliance_event` |
| Primary Activity Classification System | `primary_activity_classification_system` | String | `business_risk_compliance_event` |
### KYBCertificate (policy configuration only)
These certificate-level fields are used when configuring a
[querying policy](/concepts/governance/querying-policies) — filters over issued
certificates, not data projected into the query response.
| Field | API name | Type | Source model |
| ------------------------------------------- | --------------------------------------------- | -------- | ----------------- |
| Certificate As Of Date | `certificate_as_of_date` | Date | `kyb_certificate` |
| Certificate Attestation Timestamp | `certificate_attestation_timestamp` | Datetime | `kyb_certificate` |
| Days Since Certificate As Of Date | `days_since_certificate_as_of_date` | Integer | `kyb_certificate` |
| Is Business Identity Verification Performed | `is_business_identity_verification_performed` | Boolean | `kyb_certificate` |
| Is Business Ownership Control Performed | `is_business_ownership_control_performed` | Boolean | `kyb_certificate` |
| Is Business Risk Compliance Assessed | `is_business_risk_compliance_assessed` | Boolean | `kyb_certificate` |
## Related
## In the dashboard
Request anatomy, 200 vs 204, billing, and the X-Ref-Id header.
Check field coverage before running a billable query.
# KYC Certificate
Source: https://docs.solo.one/api-overview/querying/kyc-certificate
Identity-verification certificate for consumer onboarding
The **KYC Certificate** is a reusable identity-verification certificate bundle
for consumer onboarding. It replaces per-bank KYC intake with a network-issued
attestation: instead of each institution re-running document capture, biometric
checks, and identity corroboration for the same person, a query assembles the
verification work already furnished by network participants into a single
certificate.
| | |
| -------------- | -------------------------------------------------------------------------------------- |
| **Category** | Identity Verification |
| **Use case** | Customer Onboarding |
| **Subject** | Consumer |
| **Operations** | `POST /v1/products/kyc_certificate/query`, `POST /v1/products/kyc_certificate/furnish` |
## What's in the certificate
A KYC certificate consolidates up to nine **sub-products**, each a block in the
query response with its own `assertions` (what was attested, and when) and
`data` (the supporting attributes):
| Sub-product | Response key | What it attests |
| ---------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
| Document capture | `document_capture` | An identity document was captured, with its type, number, and dates. |
| Document review | `document_review` | The document's attributes were reviewed — tamper and machine-readable-data checks. |
| Biometric capture | `biometric_capture` | A biometric artifact (e.g. a selfie) was captured. |
| Biometric review | `biometric_review` | The biometric was compared against a reference, with quality and outcome. |
| Liveness capture | `liveness_capture` | Liveness evidence was captured. |
| Liveness review | `liveness_review` | The liveness evidence was reviewed, with outcome and confidence tier. |
| Address capture | `address_capture` | A residential address was captured. |
| Address verification | `address_verification` | The address was verified, with methods and per-source match counts. |
| Identity corroboration | `identity_corroboration` | CIP-style corroboration — SSN, name, and DOB matches across sources. |
Every populated sub-product carries the `furnishing_entity_id` of the
participant whose data backed it and the `attestation_id` of their attestation,
so the certificate is auditable down to its sources.
## Querying
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
The request follows the standard [query anatomy](/api-overview/querying/overview):
identify the subject with `consent_id` (or `consumer_id` for direct
permissible-purpose lookups), and scope the read with `network_ids` plus an
optional `policy_id` and `furnishing_entity_ids`.
A `200 OK` means a certificate was issued:
```json theme={null}
{
"certificate_id": "1c9f4e2a-…",
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"result": {
"meta": {
"network_id": "9f1c0c2e-…",
"policy_id": "5e7d2a14-…"
},
"document_capture": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"document_capture_assertion": true,
"document_capture_timestamp": "2026-04-15T00:00:00Z"
},
"data": {
"document_artifact": "passport_scan.pdf",
"document_type": "passport",
"document_issuing_state": "government",
"document_number": "934712385",
"document_issue_date": "2021-03-02",
"document_expiration_date": "2031-03-02",
"document_capture_method": "mobile_scan"
}
},
"document_review": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"document_attribute_review_assertion": true,
"document_attribute_review_timestamp": "2026-04-16T00:00:00Z"
},
"data": {
"document_review_method": "automated_ocr",
"document_tamper_review_performed": true,
"document_tamper_indicators_observed": false,
"is_document_machine_readable_data_validation_performed": true,
"is_document_machine_readable_data_validation_consistent_with_document_face": true
}
},
"biometric_capture": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"biometric_capture_assertion": true,
"biometric_capture_timestamp": "2026-04-15T00:00:00Z"
},
"data": {
"biometric_artifact": "selfie.jpg",
"biometric_capture_method": "selfie"
}
},
"identity_corroboration": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"is_identity_corroboration_performed": true,
"identity_corroboration_timestamp": "2026-04-16T09:30:00Z"
},
"data": {
"identity_elements_corroborated": "pass",
"identity_corroboration_methods": ["database", "documentary"],
"identity_corroboration_outcomes": ["ssn_match", "name_match", "dob_match"],
"total_identity_corroboration_sources_consulted": 3,
"identity_corroboration_source_count_full_match": 3,
"identity_corroboration_source_count_inconclusive": 0,
"identity_corroboration_source_count_no_match": 0,
"identity_corroboration_source_count_partial_match": 0
}
},
"biometric_review": null,
"liveness_capture": null,
"liveness_review": null,
"address_capture": null,
"address_verification": null
}
}
```
* `certificate_id` — the issued certificate. The certificate records the
date it was issued, its attestation timestamp, and the network + policy pairs
it was resolved under.
* `query_event_id` — the billable query event id; the same value is returned in
the `X-Ref-Id` response header. Quote it to support and keep it in your audit
log.
* `result` — the consolidated certificate. Sub-products the policy didn't
require (or that lacked data) are `null`.
## How the certificate resolves per network
```mermaid theme={null}
flowchart LR
A[Resolve subject
consent_id / consumer_id] --> B[Gather furnished events
across network_ids]
B --> C[Apply querying policy
field + freshness filters]
C --> D[Pick oldest matching event
per sub-product]
D --> E{Policy requirements met?}
E -- yes --> F[Issue certificate · 200]
E -- no --> G[No certificate · 204]
```
The query gathers furnished events across every network in `network_ids` and
applies the querying policy's filters uniformly. For each sub-product it selects
the **oldest matching event** from any allowed network, and the result's
`meta.network_id` is anchored to the **first** network in your request — list
your primary network first. Which furnishers' data is considered, and which
fields you see, is further shaped by your
[entitlement](/concepts/governance/entitlement).
## When you get a 204
The endpoint declares a `204 No Content` response: *no certificate could be
created — the available data did not satisfy the policy requirements*. Concretely,
a `204` is returned when:
* a sub-product the policy requires was never furnished for this consumer in
the queried networks, or
* furnished data exists but fails the policy's filters (e.g. outside the
freshness window the policy selected), or
* the policy selected specific data fields that the resolved certificate could
not populate.
The body is empty. The response still carries the `X-Ref-Id` header, and the
query event is still recorded. To anticipate `204`s before spending a billable
query, run a [coverage check](/api-overview/querying/coverage-check) first.
## Furnishing
Participants contribute the underlying KYC data with
`POST /v1/products/kyc_certificate/furnish`:
```json theme={null}
{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
}
```
The response is `{"success": true, "submission_id": "…"}`. Bulk contribution is
also available via [file upload](/api-overview/furnishing/file-upload) and
[SFTP](/api-overview/sftp/overview); see [Furnishing](/api-overview/furnishing/overview)
for the full model.
## Field reference
The KYC certificate's data dictionary, model by model. *Field* is the display
name, *API name* is the `field_name` used in policies and coverage checks, and
*Source model* is the underlying table the value is drawn from.
### Consumer
| Field | API name | Type | Source model |
| ---------- | ------------ | ------ | ------------ |
| First Name | `first_name` | String | `consumer` |
| Last Name | `last_name` | String | `consumer` |
### DocumentCaptureEvent
| Field | API name | Type | Source model |
| ----------------------------------- | ------------------------------------- | ------- | ------------------------ |
| Attestation ID | `attestation_id` | UUID | `document_capture_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Document Captured | `is_document_captured` | Boolean | `document_capture_event` |
| Document Capture Timestamp | `document_capture_timestamp` | Date | `document_capture_event` |
| Is Document Attribute Reviewed | `is_document_attribute_reviewed` | Boolean | `document_capture_event` |
| Document Attribute Review Timestamp | `document_attribute_review_timestamp` | Date | `document_capture_event` |
### IdentityDocument
| Field | API name | Type | Source model |
| ------------------------------ | -------------------------------- | ------- | ------------------- |
| Identity Document Type | `type` | String | `identity_document` |
| Document Issue Date | `issue_date` | Date | `identity_document` |
| Document Expiration Date | `expiration_date` | Date | `identity_document` |
| Issuing Authority | `issuing_authority` | String | `identity_document` |
| Is Tampering Detected | `is_tampering_detected` | Boolean | `identity_document` |
| Are Security Features Verified | `are_security_features_verified` | Boolean | `identity_document` |
| Document Number | `number` | Integer | `identity_document` |
| Document Upload | `upload` | String | `identity_document` |
### BiometricCaptureEvent
| Field | API name | Type | Source model |
| ------------------------------------ | -------------------------------------- | ------- | ------------------------- |
| Attestation ID | `attestation_id` | UUID | `biometric_capture_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Biometric Captured | `is_biometric_captured` | Boolean | `biometric_capture_event` |
| Biometric Capture Timestamp | `biometric_capture_timestamp` | Date | `biometric_capture_event` |
| Is Biometric Attribute Reviewed | `is_biometric_attribute_reviewed` | Boolean | `biometric_capture_event` |
| Biometric Attribute Review Timestamp | `biometric_attribute_review_timestamp` | Date | `biometric_capture_event` |
| Biometric Artifact Type | `biometric_artifact_type` | String | `biometric_capture_event` |
| Biometric Artifact Image Quality | `biometric_artifact_image_quality` | String | `biometric_capture_event` |
| Biometric Artifact Subject Present | `biometric_artifact_subject_present` | Boolean | `biometric_capture_event` |
| Biometric Artifact Upload | `biometric_artifact_upload` | String | `biometric_capture_event` |
### LivenessCheckEvent
| Field | API name | Type | Source model |
| ----------------------------- | ------------------------------- | ------- | ---------------------- |
| Attestation ID | `attestation_id` | UUID | `liveness_check_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Liveness Captured | `is_liveness_captured` | Boolean | `liveness_check_event` |
| Capture Timestamp | `capture_timestamp` | Date | `liveness_check_event` |
| Is Liveness Evidence Reviewed | `is_liveness_evidence_reviewed` | Boolean | `liveness_check_event` |
| Evidence Review Timestamp | `evidence_review_timestamp` | Date | `liveness_check_event` |
| Capture Method Type | `capture_method_type` | String | `liveness_check_event` |
| Check Result | `check_result` | String | `liveness_check_event` |
| Evidence Clarity | `evidence_clarity` | String | `liveness_check_event` |
### AddressCaptureEvent
| Field | API name | Type | Source model |
| -------------------------------- | ---------------------------------- | ------- | ----------------------- |
| Attestation ID | `attestation_id` | UUID | `address_capture_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is Address Captured | `is_address_captured` | Boolean | `address_capture_event` |
| Address Capture Timestamp | `address_capture_timestamp` | Date | `address_capture_event` |
| Is Address Verified | `is_address_verified` | Boolean | `address_capture_event` |
| Address Verification Timestamp | `address_verification_timestamp` | Date | `address_capture_event` |
| Address Verification Method Type | `address_verification_method_type` | String | `address_capture_event` |
| Is Match | `is_match` | Boolean | `address_capture_event` |
### IdentityVerificationEvent
| Field | API name | Type | Source model |
| -------------------------------- | ---------------------- | -------- | ----------------------------- |
| Attestation ID | `attestation_id` | UUID | `identity_verification_event` |
| Furnishing Entity ID | `furnishing_entity_id` | UUID | `attestation` |
| Is KYC CIP | `is_kyccip` | Boolean | `identity_verification_event` |
| KYC Decision | `kyc_decision` | String | `identity_verification_event` |
| Is SSN Match | `is_ssn_match` | Boolean | `identity_verification_event` |
| Is Name Match | `is_name_match` | Boolean | `identity_verification_event` |
| Name Verified | `name_verified` | Boolean | `identity_verification_event` |
| Is DOB Match | `is_dob_match` | Boolean | `identity_verification_event` |
| SSN Verified | `ssn_verified` | Boolean | `identity_verification_event` |
| Deliverable | `deliverable` | Boolean | `identity_verification_event` |
| Identity Corroboration Timestamp | `created_at` | Datetime | `identity_verification_event` |
### KYCCertificate (policy configuration only)
These certificate-level fields are used when configuring a
[querying policy](/concepts/governance/querying-policies) — e.g. requiring a
certificate no older than 30 days. They are filters over issued certificates,
not data projected into the query response.
| Field | API name | Type | Source model |
| ----------------------------------- | ------------------------------------- | -------- | ----------------- |
| Certificate As Of Date | `certificate_as_of_date` | Date | `kyc_certificate` |
| Certificate Attestation Timestamp | `certificate_attestation_timestamp` | Datetime | `kyc_certificate` |
| Days Since Certificate As Of Date | `days_since_certificate_as_of_date` | Integer | `kyc_certificate` |
| Is Address Captured | `is_address_captured` | Boolean | `kyc_certificate` |
| Is Address Verification Performed | `is_address_verification_performed` | Boolean | `kyc_certificate` |
| Is Biometric Captured | `is_biometric_captured` | Boolean | `kyc_certificate` |
| Is Biometric Reviewed | `is_biometric_reviewed` | Boolean | `kyc_certificate` |
| Is Document Captured | `is_document_captured` | Boolean | `kyc_certificate` |
| Is Document Reviewed | `is_document_reviewed` | Boolean | `kyc_certificate` |
| Is Identity Corroboration Performed | `is_identity_corroboration_performed` | Boolean | `kyc_certificate` |
| Is Liveness Captured | `is_liveness_captured` | Boolean | `kyc_certificate` |
| Is Liveness Reviewed | `is_liveness_reviewed` | Boolean | `kyc_certificate` |
## Related
## In the dashboard
Request anatomy, 200 vs 204, billing, and the X-Ref-Id header.
Check field coverage before running a billable query.
# Network consumption
Source: https://docs.solo.one/api-overview/querying/overview
How reusing verified work from the network works, and why consent is required
**Querying** is how a participant consumes the network: it reads consolidated,
already-verified data about an entity instead of re-collecting it. A single
query brings together four concepts — a **network**, a **policy**, a **product**,
and a **consumer** (or business) — gated by **consent**.
## Why consume network data
You query to reuse work, not to "buy data." A query returns a
[trust asset](/concepts/trust/trust-assets) — an attested verification with its
provenance intact — so the outcome is operational, not informational:
* **Faster onboarding** — reuse a verified [KYC](/api-overview/querying/kyc-certificate)
or [KYB](/api-overview/querying/kyb-certificate) certificate instead of repeating
document collection and review.
* **Less manual review** — start from another institution's completed,
evidenced verification rather than from zero.
* **Do only the missing work** — a [coverage check](/api-overview/querying/coverage-check)
tells you what already exists, so you collect only what's genuinely absent.
Read it as *"check whether verified trust assets already exist for this
subject,"* not *"fetch a data record."*
## How querying works
```mermaid theme={null}
flowchart LR
C[Consumer / Business] --> Cons[Consent]
Cons --> Q[Product query]
N[Networks] --> Q
P[Querying policy] --> Q
Prod[Product] --> Q
Q --> R[Consolidated result]
Q -.-> E[Query event
X-Ref-Id]
```
Each piece plays a distinct role:
| Concept | Role in the query |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Product** | *What* you're reading (e.g. a KYC certificate). Determines the endpoint and schema. See the [product catalog](/api-overview/querying/products). |
| **Network** | *Where* you're reading from. You must be a [querier](/concepts/governance/networks#permissions--roles) of every network you include. |
| **Policy** | *Which rules* apply — the field-level [querying policy](/concepts/governance/querying-policies) governing what the query may read and how fresh it must be. |
| **Consumer / Business** | *Who* the query is about — resolved from your [consent](/concepts/identity/consent) record. |
## Anatomy of a query
Every product query is a `POST` to the product's `/query` endpoint. The request
body has two halves: **who** the query is about, and **where + under which rules**
to read.
| Field | Type | Required | Purpose |
| ----------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `consent_id` | string | one of¹ | Consent token identifying the subject and your permission to query them. See [Consent](/concepts/identity/consent). |
| `consumer_id` / `business_id` | UUID | one of¹ | Direct profile reference for permissible-purpose queries, as an alternative to `consent_id`. |
| `network_ids` | UUID\[] | yes | One or more networks to read across. You must hold the querier role in each. |
| `policy_id` | UUID | no | The [querying policy](/concepts/governance/querying-policies) applied across every network in `network_ids`. If omitted, each network's default policy is used. |
| `furnishing_entity_ids` | UUID\[] | no | Optional furnisher scope. When omitted, data from all furnishers is considered. |
¹ Provide either a `consent_id` *or* a direct profile id — not both.
Why both halves matter:
* The **subject** half (`consent_id`) carries the legal basis. Querying personal
or business data requires a permissible purpose and the subject's permission,
and the consent record is the proof. It also resolves the subject's identity so
you never repeat their personal details on each call.
* The **scope** half (`network_ids` + `policy_id`) tells the network where to
read and which field-level rules to apply. The same product behaves differently
under different policies, so the pair determines both *what data is considered*
and *how fresh and complete it must be*.
Each issued certificate records the exact network + policy pairs it was
resolved under, so the scope you pass is permanently part of the audit trail.
### Full example
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
A successful query returns `200 OK` with the consolidated result and an
`X-Ref-Id` response header:
```json theme={null}
{
"certificate_id": "1c9f4e2a-…",
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"result": {
"meta": {
"network_id": "9f1c0c2e-…",
"policy_id": "5e7d2a14-…"
},
"document_capture": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"document_capture_assertion": true,
"document_capture_timestamp": "2026-04-15T00:00:00Z"
},
"data": {
"document_artifact": "passport_scan.pdf",
"document_type": "passport",
"document_issuing_state": "government",
"document_number": "934712385",
"document_issue_date": "2021-03-02",
"document_expiration_date": "2031-03-02",
"document_capture_method": "mobile_scan"
}
},
"biometric_capture": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"biometric_capture_assertion": true,
"biometric_capture_timestamp": "2026-04-15T00:00:00Z"
},
"data": {
"biometric_artifact": "selfie.jpg",
"biometric_capture_method": "selfie"
}
},
"document_review": null,
"biometric_review": null,
"liveness_capture": null,
"liveness_review": null,
"address_capture": null,
"address_verification": null,
"identity_corroboration": null
}
}
```
The shape of `result` is product-specific — see the per-product pages for the
[KYC certificate](/api-overview/querying/kyc-certificate),
[KYB certificate](/api-overview/querying/kyb-certificate), and
[screening lists](/api-overview/querying/screening-lists).
## 200 vs 204 — "here is data" vs "nothing usable"
Product query endpoints distinguish two successful outcomes:
| Status | Meaning | Body |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ |
| `200 OK` | The query resolved and produced a usable result (e.g. a certificate was issued). | The product's response schema. |
| `204 No Content` | The query resolved, but the available data did not satisfy the policy's requirements — no certificate could be created, no usable result exists. | **Empty.** |
`204` is not an error. It means the network looked, applied your policy, and
found that the furnished data falls short of what the policy demands — for
example, a required sub-product was never furnished, or the data is older than
the policy's freshness window allows. Clients should treat `204` as "queried,
nothing to show" without inspecting product-specific fields.
A `204` response still carries the `X-Ref-Id` header, because the query
itself was resolved and recorded. To avoid paying for predictable `204`s, run
a non-billable [coverage check](/api-overview/querying/coverage-check) first.
## The `X-Ref-Id` header
Every product query response — `200` and `204` alike — includes an `X-Ref-Id`
response header. Its value is the **query event id**: the durable identifier of
this query in the network's audit trail. On `200` responses the same value also
appears in the body as `query_event_id`.
```text theme={null}
HTTP/1.1 200 OK
X-Ref-Id: 7b2d9c4e-…
Content-Type: application/json
```
Treat it like a receipt:
* **Quote it to support.** If a query produced an unexpected result, the
`X-Ref-Id` lets SOLO trace the exact request, the policy applied, and the data
considered.
* **Store it in your own audit log.** It ties your internal decision record to
the network's record of the same event.
* **Reconcile billing.** Each billable query corresponds to one query event id.
## What you get back — entitlement
The fields included in a `200` result are the intersection of:
1. what the network's [querying policy](/concepts/governance/querying-policies) allows, and
2. what you're [entitled](/concepts/governance/entitlement) to read for that entity.
Entitlement is earned per participant — typically by having furnished data for
the entity or having previously queried it. Two participants running the same
query against the same subject can therefore receive different results. If a
field you expected is missing, check entitlement before assuming the data was
never furnished. See [Entitlement](/concepts/governance/entitlement) for the
full model.
## Billing
Product query requests are **billable events**. Conservatively:
* Each call to a product's `/query` endpoint that resolves — whether it returns
`200` or `204` — is recorded as a query event and may be billed.
* The [coverage check](/api-overview/querying/coverage-check) (`POST
/v1/products/check`) is **not** billable. It exists precisely so you can
pre-flight a query without incurring a billable event.
* Pricing is defined by your network agreement; this documentation does not
state prices.
A `204 No Content` response is still a resolved query. If your integration
retries on `204`, every retry is another billable event. Use the
[coverage check](/api-overview/querying/coverage-check) to avoid querying subjects
whose data cannot satisfy your policy.
## Multi-network queries
`network_ids` accepts more than one network, letting a single call read across
every network you participate in:
```json theme={null}
{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…", "4d8a7f31-…"]
}
```
How a multi-network query resolves:
* The single `policy_id` is applied uniformly across **every** network listed.
If omitted, each network's default policy is used.
* Data is gathered from all listed networks; for each sub-product the network
selects the oldest matching event from any allowed network.
* The result's `meta.network_id` is **anchored to the first network** in
`network_ids` — list your primary network first.
* You must hold the querier role in every network listed; a network you cannot
query fails the request rather than being silently skipped.
## How consent ties in — and why it's needed
You cannot query a consumer or business without a valid `consent_id` (or a
direct profile id under an established permissible purpose).
Create a consent record for the subject and receive a `consent_id`. See
[Consent](/concepts/identity/consent).
Pass the `consent_id` in the query body. The network resolves it to the
subject and applies the right field access.
The same `consent_id` can be used for repeated queries of that subject while
the consent remains valid.
A query without a valid consent for the subject is rejected. Consent is the gate;
[entitlement](/concepts/governance/entitlement) and [policy](/concepts/governance/querying-policies) shape the
result once you're through it.
### Consent vs direct profile queries
Product query endpoints accept two identity-resolution paths:
* **`consent_id`** — the standard path. The consent token both identifies the
subject and proves your permission to query them. Use this for any flow where
the subject interacts with you (onboarding, re-verification).
* **`consumer_id` / `business_id`** — a direct reference to a profile you can
already see, for permissible-purpose queries where consent was established
through another channel. The subject must resolve within your scope or the
query returns `404`.
Both paths produce the same response shape and the same billable query event.
When in doubt, use `consent_id`.
## Query failure modes
Failures use the standard error envelope —
`{"detail": "…", "error_code": "…"}` — described in
[Errors](/home/errors).
| Status | `error_code` | Typical cause |
| --------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request` | `VALIDATION_ERROR` | The request is structurally valid but semantically wrong — e.g. a consent that doesn't resolve to a subject. |
| `401 Unauthorized` | `AUTHENTICATION_REQUIRED` | Missing, malformed, or expired bearer token. See [Authentication](/home/authentication). |
| `403 Forbidden` | `PERMISSION_DENIED` | You're not a querier of a listed network, or your token lacks the required scope. |
| `404 Not Found` | `RESOURCE_NOT_FOUND` | The consumer, business, consent, or product could not be resolved. |
| `422 Unprocessable Entity` | — | Request body failed schema validation (e.g. `network_ids` empty or missing); `detail` names the field. |
| `500 Internal Server Error` | `INTERNAL_ERROR` / `OPERATION_FAILED` | Unexpected server error. Quote the `X-Ref-Id` or `request_id` to support. |
Remember that `204 No Content` is **not** in this table — it's a success status
meaning the policy's requirements were not met by the available data.
## Integration checklist
A robust querying integration usually looks like this:
1. **Record consent once, reuse it.** One consent record covers repeated
queries of the same subject while it remains valid.
2. **Pre-flight with the coverage check** when a `204` would hurt your UX or
your budget — it's free and uses the same scope fields as the query.
3. **Handle `204` as a first-class outcome**, not an error: branch your product
flow on "no usable result" rather than retrying.
4. **Persist `query_event_id` / `X-Ref-Id`** alongside your own decision
records for audit, support, and billing reconciliation.
5. **Treat missing fields as an entitlement question first** — check what
you've furnished and what the policy exposes before raising a data issue.
## The network control plane
Reuse is safe because it is governed by independent controls — the network's
control plane. Every one of these exists today; none can be bypassed by a single
actor:
| Control | What it governs | Where it's defined |
| -------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **Consent** | Whether a subject's data may be read at all, its scope, and its expiry | [Consent](/concepts/identity/consent) (`scope`, `expires_at`, `consented_fields`) |
| **Entitlement** | Which fields a given reader may see, earned by participation | [Entitlement](/concepts/governance/entitlement) |
| **Querying policy** | What a product exposes, and how fresh it must be to count | [Querying policies](/concepts/governance/querying-policies) (freshness windows) |
| **`204` on insufficiency** | Refusal to return stale or incomplete results | This page — `204 No Content` |
A governor controls policy but not consent; a subject controls consent but not
entitlement; your own history determines entitlement but not policy. That
separation is what lets institutions reuse each other's work without anyone
losing control of their own.
## In the dashboard
## Related concepts
The catalog of what you can query.
Pre-flight a query without a billable event.
The other half of the network — contributing data.
A hands-on walkthrough.
# Products
Source: https://docs.solo.one/api-overview/querying/products
The standardized data sets you query and furnish on the network
A **product** is a standardized, named data set that you can **query** (read) and
— for certificate products — **furnish** (write) through the network. Products
give every participant a common schema for a given verification domain, so data
furnished by one participant can be queried by another without bespoke
integration.
## The catalog
Four products are queryable today. The exact set available to you depends on the
networks you belong to.
| Product | What it answers | Subject | Query path |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------- |
| [KYC Certificate](/api-overview/querying/kyc-certificate) | "Has this consumer's identity been verified — documents, biometrics, liveness, address, and corroboration?" | Consumer | `POST /v1/products/kyc_certificate/query` |
| [KYB Certificate](/api-overview/querying/kyb-certificate) | "Has this business been verified — identity, ownership & control, and risk/compliance?" | Business | `POST /v1/products/kyb_certificate/query` |
| [Bank-Specific Bad Actor List](/api-overview/querying/screening-lists) | "Has this consumer violated a documented policy of this sponsor bank's subnetwork?" | Consumer | `POST /v1/products/bank_specific_bad_actor_list/query` |
| [Cross-Bank Financial Crimes Watch List](/api-overview/querying/screening-lists) | "Is this consumer associated with suspicious financial-crimes activity reported across participating banks?" | Consumer | `POST /v1/products/cross_bank_financial_crimes_watch_list/query` |
They fall into two families:
* **Certificates** are consolidated, multi-attribute verification results
assembled from data furnished by network participants. Querying one can
*issue* a reusable certificate for the subject. Certificate queries return
`204 No Content` when the available data cannot satisfy the policy.
* **Screening lists** are read-only yes/no checks against lists of flagged
individuals. They return a listing indicator plus a small set of context
fields, never a certificate — and always `200 OK`, with a clean result
expressed as `"is_listed": false`.
| Behavior | Certificates | Screening lists |
| -------------------- | ---------------------- | ----------------------------- |
| Furnishable via API | Yes — `/furnish` | No (query-only) |
| Issues an artifact | Yes — `certificate_id` | No |
| Empty result | `204 No Content` | `200` with `is_listed: false` |
| Billable query event | Yes | Yes |
## The query / furnish pattern
**Read** consolidated data for an entity, drawn from what authorized
participants have furnished. Requires a [consent](/concepts/identity/consent) ID
or a direct profile reference.
**Contribute** verified data for an entity into a network, making it available
for future queries by entitled participants.
```text theme={null}
POST /v1/products/{product}/query
POST /v1/products/{product}/furnish (certificate products)
```
Certificate products accept structured JSON furnishing via the API, and bulk
furnishing via [file upload](/api-overview/furnishing/file-upload) or
[SFTP](/api-overview/sftp/overview). The two screening lists are query-only over
the API; the data behind them is contributed through governed furnishing flows.
There is also one utility endpoint that is **not** a product query:
`POST /v1/products/check`, the non-billable
[coverage check](/api-overview/querying/coverage-check), which reports whether the
furnished data for a subject can satisfy a policy *before* you run a billable
query.
## How products relate to networks and policies
```mermaid theme={null}
flowchart LR
Prod[Product
schema] --> NP[Offered in a network]
Net[Network] --> NP
NP --> Pol[Querying policy
field rules + freshness]
Pol --> Q[Query result]
```
A product defines *what* data exists — its models, fields, and types. A
[network](/concepts/governance/networks) decides *which* products it offers to
its members. A [querying policy](/concepts/governance/querying-policies)
defines, per network, *which parts* of a product a query may read and under
what conditions (e.g. how recent a verification must be). The same product can
behave differently in two networks because each network attaches its own
policies.
This is why every query carries both `network_ids` and a `policy_id`: the
product determines the endpoint and response schema, while the network + policy
scope determines what actually comes back. What you're
[entitled](/concepts/governance/entitlement) to read narrows it further.
Each product's schema is defined as a set of **models** — named groups of typed
fields such as `DocumentCaptureEvent` or `BusinessRiskComplianceEvent`. These
model and field names are the shared vocabulary across the platform: policies
select them, the [coverage check](/api-overview/querying/coverage-check) reports
against them, and the per-product field reference tables document them. When a
policy "requires liveness within 30 days," it is expressing a filter over a
product model's fields — the same fields you'll see in the query response.
## Querying a product
A product query reads consolidated data for one entity. It needs a **consent
ID** (or direct profile id) identifying the subject, and a **network + policy**
scope:
```json theme={null}
{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}
```
See [Querying](/api-overview/querying/overview) for the full request anatomy,
`200` vs `204` semantics, the `X-Ref-Id` audit header, and billing notes.
## Furnishing a product
Furnishing contributes one or more records for an entity into a network:
```json theme={null}
{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
}
```
The network matches each record to an entity and stores the furnished data under
your organization, building your [entitlement](/concepts/governance/entitlement)
to query it back later. See [Furnishing](/api-overview/furnishing/overview).
## Choosing a product
| If you need to… | Use |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Verify a consumer's identity at onboarding | [KYC Certificate](/api-overview/querying/kyc-certificate) |
| Verify a business and its beneficial owners | [KYB Certificate](/api-overview/querying/kyb-certificate) |
| Screen a consumer against your sponsor bank's subnetwork history | [Bank-Specific Bad Actor List](/api-overview/querying/screening-lists) |
| Screen a consumer for cross-bank financial-crimes signals | [Cross-Bank Financial Crimes Watch List](/api-overview/querying/screening-lists) |
| Know whether a query can succeed before paying for it | [Coverage Check](/api-overview/querying/coverage-check) |
In practice these compose: an onboarding flow might run both screening lists
first, then a coverage check, then the KYC certificate query — three billable
list/certificate queries at most, with the free check deciding whether the
certificate query is worth running.
## In the dashboard
## Product deep dives
Consolidated consumer identity verification — nine sub-products, full field
reference.
Business identity, ownership & control, and risk/compliance in one
certificate.
Bank-specific bad actor and cross-bank financial crimes watch lists.
Non-billable pre-flight: can this query succeed under this policy?
# Screening Lists
Source: https://docs.solo.one/api-overview/querying/screening-lists
Bank-specific bad actor and cross-bank financial crimes watch lists
SOLO offers two **screening list** products in the Fraud & Financial Crime
category. Both answer a narrow question — *is this consumer on a list?* — and
return a listing indicator plus a small set of context fields. Unlike
[certificates](/api-overview/querying/kyc-certificate), screening list queries never
issue a reusable artifact; they are point-in-time checks.
| | Bank-Specific Bad Actor List | Cross-Bank Financial Crimes Watch List |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **What it is** | A sponsor-bank-scoped list of specific individuals who, as prior customers of one or more of the bank's fintech partners, violated one or more specific documented bank policies. | A cross-bank shared consortium that extends beyond one sponsor-bank network and focuses on sharing suspicious financial-crimes activity. Governed by banks as defined by 31 CFR 1020.100(d) under 314(b) permissible purpose. |
| **Scope** | Your sponsor bank's network only — results never cross network boundaries. | All participating banks — visibility spans the consortium. |
| **Use case** | Bad Actor Screening — catching repeat offenders re-applying within the same bank's subnetworks. | Watch List Screening — surfacing inter-bank financial-crimes signals. |
| **Subject** | Consumer | Consumer |
| **Query path** | `POST /v1/products/bank_specific_bad_actor_list/query` | `POST /v1/products/cross_bank_financial_crimes_watch_list/query` |
The key difference is **who can see what**. The bad actor list is scoped to one
sponsor bank: a consumer flagged in Bank A's network is invisible to Bank B.
The watch list is the opposite by design — it exists so that suspicious
financial-crimes activity observed at one bank is visible to the others, under
the 314(b) information-sharing framework.
Both lists are **query-only** over the API. The events behind them are
contributed by network participants through governed furnishing flows.
## Querying
Both endpoints take the standard [query request](/api-overview/querying/overview):
a `consent_id` (or `consumer_id` for permissible-purpose lookups) plus the
`network_ids` scope, with optional `policy_id` and `furnishing_entity_ids`.
### Bank-Specific Bad Actor List
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/bank_specific_bad_actor_list/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": null,
"is_listed": false,
"bad_actor_list_placement_date": null,
"bad_actor_list_reason_code": null
}
```
| Response field | Type | Meaning |
| ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------- |
| `query_event_id` | UUID | The billable query event id — also in the `X-Ref-Id` header. |
| `consumer_id` | UUID | The resolved subject. |
| `furnishing_entity_id` | UUID, nullable | The participant whose listing backed the result, when listed. |
| `is_listed` | boolean | Whether the consumer appears on the list. |
| `bad_actor_list_placement_date` | date, nullable | When the consumer was placed on the list. |
| `bad_actor_list_reason_code` | string, nullable | The documented policy-violation reason (e.g. `account_abuse`, `fraud`, `policy_violation`). |
When the consumer is listed, the context fields are populated:
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": "e1d2c3b4-…",
"is_listed": true,
"bad_actor_list_placement_date": "2025-11-04",
"bad_actor_list_reason_code": "account_abuse"
}
```
### Cross-Bank Financial Crimes Watch List
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/cross_bank_financial_crimes_watch_list/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": null,
"is_listed": false,
"entity_level_adverse_action_eligible_indicator": null,
"watch_list_placement_date": null
}
```
| Response field | Type | Meaning |
| ------------------------------------------------ | ----------------- | ----------------------------------------------------------------------------- |
| `query_event_id` | UUID | The billable query event id — also in the `X-Ref-Id` header. |
| `consumer_id` | UUID | The resolved subject. |
| `furnishing_entity_id` | UUID, nullable | The participant whose listing backed the result, when listed. |
| `is_listed` | boolean | Whether the consumer appears on the watch list. |
| `entity_level_adverse_action_eligible_indicator` | boolean, nullable | Whether the signal is eligible to support adverse action at the entity level. |
| `watch_list_placement_date` | date, nullable | When the consumer was placed on the watch list. |
Screening list queries always return `200 OK` — a clean result is expressed as
`"is_listed": false`, not as a `204`. (The `204` semantics on the
[certificate endpoints](/api-overview/querying/kyc-certificate#when-you-get-a-204)
signal that *no certificate could be created*; a list check always produces a
usable answer.) Both queries are billable events and carry the
[`X-Ref-Id` header](/api-overview/querying/overview#the-x-ref-id-header).
## How matching works
Subjects are matched against the lists using the consumer identity behind your
[consent](/concepts/identity/consent) record. Each list defines its match
inputs — SSN and date of birth are mandatory; name, phone, and email refine the
match:
| Match input | API name | Type | Required |
| ---------------------- | ------------------------ | ------ | -------- |
| Social Security Number | `social_security_number` | String | Yes |
| Date of Birth | `date_of_birth` | Date | Yes |
| First Name | `first_name` | String | No |
| Last Name | `last_name` | String | No |
| Phone Number | `phone_number` | String | No |
| Email | `personal_email` | String | No |
These inputs are identical for both lists.
## Field reference
The list events behind each product, as defined in the product schema. *Field*
is the display name, *API name* is the `field_name` used in policies, and
*Source model* is the underlying table.
### Bank-Specific Bad Actor List — BadActorEvent
| Field | API name | Type | Source model |
| ----------------------------- | ------------------------------- | ------ | ----------------- |
| Bad Actor Reason Code | `bad_actor_reason_code` | String | `bad_actor_event` |
| Bad Actor Reason Definition | `bad_actor_reason_definition` | String | `bad_actor_event` |
| Bad Actor List Placement Date | `bad_actor_list_placement_date` | Date | `bad_actor_event` |
Reason codes are drawn from a documented set — `account_abuse`, `fraud`, and
`policy_violation` — and each listing carries the bank's definition of the
violated policy in `bad_actor_reason_definition`.
### Cross-Bank Financial Crimes Watch List — FinancialCrimesWatchEvent
| Field | API name | Type | Source model |
| -------------- | ---------------- | ------ | ------------------------------ |
| Event Date | `event_date` | Date | `financial_crimes_watch_event` |
| Event Category | `event_category` | String | `financial_crimes_watch_event` |
| Signal Level | `signal_level` | String | `financial_crimes_watch_event` |
Event categories include `money_laundering`, `terrorist_financing`, and
`fraud`; signal levels are `high`, `medium`, or `low`. A network's
[querying policy](/concepts/governance/querying-policies) can restrict which
categories and signal levels a query considers — for example, only
`high`-signal events.
## Compliance posture
Watch-list signals are screening inputs, not adjudications. Whether a listing
can support adverse action depends on your program's compliance framework —
the cross-bank list operates under 314(b) permissible purpose, and the
`entity_level_adverse_action_eligible_indicator` field exists precisely
because not every signal qualifies. Route hits to your compliance review
process.
## Related
Request anatomy, billing, and the X-Ref-Id header.
The full product catalog.
# Workbook Format
Source: https://docs.solo.one/api-overview/sftp/csv-format
File format requirements for SFTP data ingestion
Every file ingested through the SFTP channel is an Excel workbook (`.xlsx`)
with the same physical layout, regardless of
[category](/api-overview/sftp/schemas). This page is the authoritative reference
for that layout: row structure, header matching, cell types, and the
formatting mistakes that account for nearly all rejected rows.
## File format
* **`.xlsx` only** — the Office Open XML format. Legacy `.xls` and plain CSV
files cannot be parsed by the workbook readers; re-save them as `.xlsx`.
* **First sheet only** — the ingester reads the workbook's first sheet and
ignores all others. If your data lives on another tab, move it to position
one.
## Row structure
| Row | Purpose |
| ---------- | ----------------------------------------------- |
| **Row 1** | Banner row — skipped entirely during processing |
| **Row 2** | Column headers — defines the field mapping |
| **Row 3+** | Data rows — each row is processed independently |
Row 1 is reserved for a human-readable banner (workbook title, version,
instructions). It is never parsed — but it must *exist*. Headers in row 1
with data starting at row 2 is the single most common formatting error: the
ingester will read your headers as a banner and your first data row as
headers.
A workbook with no header row at all (fewer than two rows) is rejected
outright.
## Header normalization
Column headers in row 2 are normalized before matching:
1. Leading and trailing whitespace is stripped.
2. The text is converted to **lowercase**.
3. Every run of non-alphanumeric characters (spaces, punctuation, symbols —
however many in a row) is collapsed to a single **underscore** (`_`).
4. Leading and trailing underscores are stripped.
This means the following headers are all equivalent:
| Raw Header | Normalized |
| --------------------------------- | ------------------------------- |
| `KYC Policy Name` | `kyc_policy_name` |
| `Subnetwork #` | `subnetwork` |
| `Date of Birth` | `date_of_birth` |
| `End Date (if Applicable)` | `end_date_if_applicable` |
| `Business Tax Identifier (Value)` | `business_tax_identifier_value` |
| `Permitted Purpose Scope(s)` | `permitted_purpose_scope_s` |
Fields are matched by normalized header name, **never by position** — you can
reorder columns freely. Three consequences of name-based matching:
* **Unknown headers are ignored with a warning.** Extra columns don't break
ingestion; they're simply skipped. But note this cuts both ways — a
*misspelled* required header is an unknown header, and the workbook will be
rejected for the missing required one.
* **Duplicate headers reject the whole workbook.** Two columns that normalize
to the same field (e.g. `First Name` and `first_name`) are ambiguous; the
error names both column positions.
* **Missing required headers reject the whole workbook.** The error lists
every missing header. This is a file-level failure — fix the header row and
re-upload. See [Upload Categories](/api-overview/sftp/schemas) for which headers
each category requires.
"Required column" means the **header** must be present in row 2. Whether an
individual *cell* may be blank is a per-field rule — see the
required/optional markers in the [category reference](/api-overview/sftp/schemas).
## Cell types and formats
| Type | Accepted input | Recommended | Notes |
| ------------------------------------ | ----------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **String** | Text cells; numeric cells are coerced | Text-formatted cells | A numeric cell like `123.0` is read as `"123"`. **Excel drops leading zeros in numeric cells** — format identifier columns (SSN, tax IDs) as Text. |
| **Date** | Excel date/datetime cells, or ISO 8601 text (`2026-01-15`) | Native Excel date cells | Text in other formats (`01/15/2026`, `Jan 15, 2026`) is rejected. Only the date portion is used — `2026-01-15T10:30:00` parses, but the time is discarded. |
| **Application date** | Same as Date | Native Excel date cells | Parsed as a calendar date and normalized; time-of-day is not preserved. Drives [policy resolution](/concepts/governance/furnishing-policies), so use the real application date. |
| **Boolean** (policy operation flags) | `true`/`false`, `yes`/`no`, `y`/`n`, `1`/`0` (case-insensitive), or numeric `1`/`0` cells | `true` / `false` | A **blank flag cell is read as `false`**. Anything else (e.g. `enabled`) rejects the row. |
| **Empty** | Leave the cell blank | — | Optional fields deserialize to null. A blank cell in a required field rejects that row. |
## Rows that are skipped on purpose
Two kinds of rows are silently skipped — they don't count as errors:
* **Example rows.** Rows whose identifier column starts with **`Ex.`** are
treated as template examples. The identifier column depends on the
category: `File #` for data workbooks, `Subnetwork #` for subnetworks, and the
policy-number column (`KYC Policy` / `KYB Policy`) for policy workbooks.
Use these to keep in-workbook guidance without affecting ingestion.
* **Blank rows.** Rows missing their identifying values entirely — a data row
with no file number and no subnetwork name, a subnetwork row with no
subnetwork name, a policy row with no policy name — are treated as empty padding and
skipped.
## Formula cells
Workbooks are read using **cached formula results**, not live evaluation. If
a cell contains a formula that has never been calculated and saved (typical
when the file was generated programmatically and never opened in Excel), the
ingester detects the uncached formula and **rejects that row**, listing the
offending column names in the error — it will not silently treat the cell as
blank.
To avoid this: open and save the workbook in Excel with calculation enabled,
or write literal values (or pre-computed cached values) when generating files
programmatically.
## Common mistakes
| Mistake | What happens | Fix |
| ------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Headers in row 1, data from row 2 | Headers read as banner; first data row read as headers → "missing required headers" | Put a banner (or blank row) in row 1, headers in row 2 |
| Saved as `.xls` or `.csv` | File can't be parsed | Re-save as `.xlsx` |
| Data on the second sheet | Sheet ignored; first sheet (often empty) is parsed | Move data to the first sheet |
| SSN/EIN in a numeric cell | Leading zeros silently dropped → wrong identifier, wrong entity match | Format identifier columns as Text |
| US-style date text (`01/15/2026`) | Row rejected: expected an ISO date | Use Excel date cells or `2026-01-15` |
| Misspelled header (`plicy_name`) | Treated as unknown column; workbook rejected for the missing required header | Copy headers from the [templates](/api-overview/sftp/schemas) |
| Two columns normalizing to one field | Whole workbook rejected as duplicate header | Delete the duplicate column |
| Formula never calculated | Row rejected, error lists the columns | Open + save in Excel, or write literal values |
| Real data prefixed `Ex.` | Row silently skipped as a template example | Remove the `Ex.` prefix |
| Blank operation-flag cell expected to mean "true" | Read as `false` | Set flags explicitly: `true` / `false` |
Header problems are structural — without the right headers the ingester
can't trust its reading of *any* row, so it fails fast with a list of
what's missing or duplicated. Cell-level problems are local — each data
row is parsed and ingested independently, so a bad date in row 17 produces
one row error while rows 3–16 and 18+ proceed normally.
The dashboard's uploads view reports per-row outcomes, including the
spreadsheet row number (counted as Excel displays it, banner and header
included) and a human-readable message naming the offending column. Fix
the rows and re-upload under a new filename — data rows
[upsert on natural keys](/api-overview/sftp/overview#what-happens-on-upload),
so corrected rows update rather than duplicate.
For the exact columns each category expects, continue to
[Upload Categories](/api-overview/sftp/schemas).
# SFTP Getting Started
Source: https://docs.solo.one/api-overview/sftp/getting-started
Send your first workbook to the SOLO Network over SFTP
This guide walks you through your first SFTP upload end-to-end — from getting credentials to confirming that the network ingested your data. Plan on about 15 minutes.
## What you'll need
SFTP authentication is delegated to WorkOS. Your SOLO account manager will help you (or your WorkOS admin) mint a key under your organization. The key string itself is the SFTP password.
Treat the API key like any other credential. Store it in a secrets manager and never commit it to source control. The key — not the username — is what grants access: anyone holding it can upload as your organization.
Your email is the SFTP username. It identifies *who* is connecting and selects the environment via an optional plus-tag. The organization your uploads are attributed to is determined by the **API key's owning organization**, not by the email — so a valid key is required no matter what username you present.
Anything that speaks SFTP works — the `sftp` command from OpenSSH, [FileZilla](https://filezilla-project.org/), [Cyberduck](https://cyberduck.io/), or a library such as Python's [Paramiko](https://www.paramiko.org/).
## Step 1 — Pick an environment
We recommend doing your first upload against **sandbox**. Sandbox is a fully isolated copy of the network: nothing you upload there reaches production data, so it's the right place to learn the format and verify your pipeline.
You select an environment by appending a plus-tag to the local part of your email:
| Environment | Username Format | Example |
| ----------- | -------------------------- | ---------------------------- |
| Sandbox | `email+sandbox@domain.com` | `alice+sandbox@yourbank.com` |
| Production | `email@domain.com` | `alice@yourbank.com` |
Each environment writes to its own storage bucket and database; there is no crossover. Usernames are case-insensitive — `Alice@YourBank.com` and `alice@yourbank.com` are the same user.
## Step 2 — Connect
The SFTP host is the same for every environment:
```
sftp.solo.one:22
```
```bash sftp (CLI) theme={null}
sftp alice+sandbox@yourbank.com@sftp.solo.one
# When prompted, paste your WorkOS API key as the password.
```
```python Python (paramiko) theme={null}
import paramiko
transport = paramiko.Transport(("sftp.solo.one", 22))
transport.connect(
username="alice+sandbox@yourbank.com",
password="sk_live_your_workos_api_key",
)
sftp = paramiko.SFTPClient.from_transport(transport)
# ... upload here ...
sftp.close()
transport.close()
```
```text ~/.ssh/config theme={null}
Host solo-sftp-sandbox
HostName sftp.solo.one
User alice+sandbox@yourbank.com
Port 22
```
On a successful connection you'll land at the root of your organization's upload area.
Your SFTP session is **upload-only**: you can `put` files into the category directories and create directories, but `ls`, `get`, overwriting, and deleting are not permitted. Don't be surprised when a directory listing is denied — that's policy, not a fault. Verification happens in the dashboard (Step 6), not over SFTP.
If the server rejects you immediately, the most common causes are (1) the API key has been revoked, (2) your organization isn't provisioned in SOLO yet, or (3) the plus-tag is misspelled (only `+sandbox`, `+prod`, or no tag are accepted). Re-check the username and key, then reach out to your account manager if it persists.
## Step 3 — Download a template
Every upload is an Excel workbook (`.xlsx`) that lands in one of five category directories. Workbook templates are available on the [Upload Categories](/api-overview/sftp/schemas) page — they include the required header row, an example row, and the right column names already in place.
For this walkthrough, start with the simplest path: defining a **KYC certificate policy** that a furnisher can later attach data to.
KYC Certificate Policy Template (.xlsx)
Policy and subnetwork workbooks define network configuration, so they are accepted only from the network's [governor](/concepts/governance/network-governance). If your organization is a furnisher but not a governor, start with a `kyc_furnish_data` workbook instead — the steps below are the same, only the directory and columns differ.
## Step 4 — Fill it in
Open the template in Excel (or any other spreadsheet app) and add one row of real data below the example row:
| `kyc_policy_name` | `start_date` | `entity_type` | `document_capture` | `liveness_capture` | `address_verification` |
| --------------------- | ------------ | ------------- | ------------------ | ------------------ | ---------------------- |
| First KYC Test Policy | `2026-01-01` | `Consumer` | `true` | `true` | `true` |
A few things to know about the workbook format ([full reference](/api-overview/sftp/csv-format)):
* Only the **first sheet** of the workbook is read.
* **Row 1** is a banner — anything you put there is ignored.
* **Row 2** must contain the column headers.
* **Row 3 and below** are data rows. Each row is processed independently, so a single bad row won't fail the rest of the file.
* Headers are normalized (lowercased, non-alphanumerics collapsed to `_`), so `KYC Policy Name`, `kyc_policy_name`, and `KYC-Policy-Name` are all equivalent.
* Rows whose identifier column starts with `Ex.` are treated as in-workbook examples and skipped.
Save the file as `first-kyc-test.xlsx`.
## Step 5 — Upload
Drop the workbook into the directory that matches its category:
```bash sftp (CLI) theme={null}
sftp> cd kyc_cert_policy
sftp> put first-kyc-test.xlsx
Uploading first-kyc-test.xlsx to /kyc_cert_policy/first-kyc-test.xlsx
sftp> bye
```
```python Python (paramiko) theme={null}
sftp.put("first-kyc-test.xlsx", "/kyc_cert_policy/first-kyc-test.xlsx")
```
That's it — there are no intermediate folders to manage. The category directory **is** the schema selector: a file in `kyc_cert_policy/` is parsed as a KYC certificate policy workbook, a file in `subnetworks/` as a subnetworks workbook, and so on.
Pick a fresh filename for every upload (a date or batch number works well). Sessions can't overwrite or delete existing files, so re-using a name from a previous drop will be rejected.
## Step 6 — Verify ingestion
Ingestion is event-driven and asynchronous — processing starts within seconds of the file landing, and a small workbook typically completes in well under a minute.
The cleanest way to check is the [SOLO dashboard](https://app.solo.one): the **Data → Uploads** page lists every workbook your organization has sent, along with row-level success, filtered, and error counts. From there you can also drill into the resulting records (policies, subnetworks, furnish events) to confirm they showed up where you expected.
Re-sending data is safe at the row level. Data rows upsert on their natural keys (SSN for consumers; tax identifier + jurisdiction for businesses), so a corrected re-upload updates records rather than double-creating them. Re-uploaded **policy** or **subnetwork** rows that collide with existing names will surface per-row name-conflict errors instead — version the name (e.g. `First KYC Test Policy v2`) if you intend a new policy.
## Common first-upload issues
Confirm the username is your full email address (with `+sandbox` for sandbox), and the password is the literal WorkOS API key string — no `Bearer` prefix, no quotes. If you rotated the key recently, the old value is rejected; a freshly revoked key may take up to a minute to be refused everywhere, since auth results are briefly cached.
Working as intended — SFTP access is upload-only. You can `put` new files and create directories; everything else is denied. Use the dashboard to inspect what you've uploaded, and use a new filename rather than overwriting an old one.
The first time you connect from a new machine your SSH client doesn't know the server's host key yet. Either accept the fingerprint at the prompt, or pre-populate your `known_hosts`:
```bash theme={null}
ssh-keyscan sftp.solo.one >> ~/.ssh/known_hosts
```
Open the workbook in Excel and confirm:
1. Headers are in **row 2** (not row 1), and there are no merged cells above them.
2. The identifier column on every real data row is **not** prefixed with `Ex.` (those are skipped as examples).
3. The file was saved as `.xlsx`, not `.xls` or `.csv`.
4. For data workbooks: every row's `subnetwork_name` matches a subnetwork the governor has configured, and the `application_date` falls inside an active policy window — rows outside every window are **filtered** (skipped), which is reported separately from errors.
The Uploads page in the dashboard surfaces the row-level reason for any failures.
Configuration categories (`kyc_cert_policy`, `kyb_cert_policy`, `subnetworks`) are governor-only. The file itself uploads fine, but every row is rejected at ingest if your organization doesn't govern the target network. Furnishers should upload to the data categories instead.
Workbooks are read using cached formula results. If a workbook was generated programmatically and never opened in Excel, formula cells may have no cached value — the row is rejected and the error names the offending columns. Open and save the file in Excel (or set cached values when generating it) before uploading.
## Where to go next
The full mental model — categories, environments, how routing works.
Required and optional columns for every category, plus downloadable templates.
Detailed rules for headers, data types, and the row 1 banner convention.
Prefer to integrate over HTTPS? The same operations are available as REST endpoints.
# SFTP Overview
Source: https://docs.solo.one/api-overview/sftp/overview
Bulk data ingestion via SFTP file uploads
The SOLO Network supports bulk data ingestion via SFTP. Upload Excel workbooks
to your organization's SFTP endpoint, and each file is automatically routed to
the right ingestion workflow, validated row by row, and written into the
network — through the **same furnishing pipeline** that backs the
[REST channels](/api-overview/furnishing/overview). SFTP is a transport, not a
separate system: a record dropped over SFTP and the same record furnished over
HTTPS produce identical network data.
Choose SFTP when your data already leaves your systems as files on a schedule.
Core-banking platforms and data warehouses can usually target an SFTP endpoint
with no custom code, which makes this the natural channel for nightly or
monthly batch drops.
## Connecting
Connect to `sftp.solo.one` on port 22. Your **email address** is the username
and your organization's **WorkOS API key** is the password:
```bash theme={null}
sftp your-email@yourbank.com@sftp.solo.one
```
For the sandbox environment, append `+sandbox` to the local part of your email:
```bash theme={null}
sftp your-email+sandbox@yourbank.com@sftp.solo.one
```
Or using an SFTP library:
```python Python (paramiko) theme={null}
import paramiko
transport = paramiko.Transport(("sftp.solo.one", 22))
transport.connect(
username="alice@yourbank.com",
password="sk_live_your_workos_api_key",
)
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.put("policies.xlsx", "/kyc_cert_policy/policies.xlsx")
sftp.close()
transport.close()
```
```bash SSH config theme={null}
# ~/.ssh/config
Host solo-sftp
HostName sftp.solo.one
User alice@yourbank.com
Port 22
```
### How authentication works
Every connection is verified live, not against a stored password:
1. The username is parsed into an email and an environment tag
(`+sandbox` → sandbox; no tag → production). Usernames are
case-insensitive. An unrecognized tag is rejected outright.
2. The password is validated as a **WorkOS API key** against WorkOS. Revoked
or malformed keys are rejected.
3. The key's owning WorkOS organization — not anything in the username — is
resolved to your SOLO organization. Your session is scoped to that
organization's isolated storage area in the selected environment.
Because authorization derives from the **API key**, the key is the credential
that matters: rotating or revoking it in WorkOS immediately cuts off SFTP
access (allow up to a minute for cached sessions to expire). Contact your
SOLO account manager to provision keys.
SFTP sessions are **upload-only**. You can `put` files and create
directories, but listing, downloading, overwriting, and deleting are not
permitted. Upload each file under a name you haven't used before, and use
the SOLO dashboard — not the SFTP session — to confirm what was ingested.
## Directory layout
Files are uploaded to one of five category directories directly under the
session root:
```
{category}/{filename}.xlsx
```
For example:
```
kyc_cert_policy/kyc-policies-2026.xlsx
kyb_cert_policy/kyb-policies.xlsx
subnetworks/network-subnetworks.xlsx
kyc_furnish_data/consumer-batch-jan.xlsx
kyb_furnish_data/business-batch-jan.xlsx
```
The directory **is** the schema selector — it tells the ingester how to parse
the workbook:
| Category | Directory | Description |
| ---------------------- | ------------------- | ---------------------------------------------------------- |
| KYC Certificate Policy | `kyc_cert_policy/` | Define KYC verification policies with operation flags |
| KYB Certificate Policy | `kyb_cert_policy/` | Define KYB verification policies with operation flags |
| Subnetworks | `subnetworks/` | Configure network subnetworks linking KYC and KYB policies |
| KYC Furnish Data | `kyc_furnish_data/` | Consumer onboarding records for KYC certificates |
| KYB Furnish Data | `kyb_furnish_data/` | Business onboarding records for KYB certificates |
See [Upload Categories](/api-overview/sftp/schemas) for the expected columns in
each category.
## What happens on upload
Ingestion is event-driven — there is no polling window to wait for:
```mermaid theme={null}
flowchart LR
U[You: sftp put] --> S[(Org-scoped storage)]
S -->|object-created event| W[Category ingestion workflow]
W --> P[Parse workbook row by row]
P --> F[Furnishing pipeline: validate, resolve, persist]
F --> D[(Network data)]
W --> E[Furnish event recorded with row-level results]
```
1. **Upload.** The file lands in your organization's isolated storage area,
prefixed by environment and organization.
2. **Trigger.** The storage event fires the ingestion workflow for the file's
category — within seconds of the upload completing.
3. **Parse.** The workbook is read: first sheet only, row 1 treated as a
banner, row 2 as headers, row 3+ as data
([format details](/api-overview/sftp/csv-format)).
4. **Ingest.** Each row runs through the same furnishing pipeline as the API
channels. For data categories that means subnetwork routing and
[furnishing-policy resolution](/concepts/governance/furnishing-policies)
by `subnetwork_name` + `application_date`; for configuration categories
(policies, subnetworks) it means creating governor-controlled network
configuration.
5. **Record.** The outcome — total rows, successes, filtered rows, failures
with per-row messages — is recorded as a furnish event you can review in
the dashboard.
Key behaviors:
* **Row-level processing** — each row succeeds or fails independently. A
single invalid row never sinks the rest of the file.
* **Filtered is not failed** — data rows whose `application_date` falls
outside every applicable policy window are skipped deliberately and
reported as filtered.
* **Header normalization** — headers are lowercased and non-alphanumeric runs
collapse to underscores, so column order and cosmetic formatting don't
matter.
* **Example-row skipping** — rows whose identifier column starts with `Ex.`
are treated as in-workbook examples and skipped.
* **Safe re-runs** — data rows upsert on their natural keys (SSN for
consumers; tax identifier and jurisdiction for businesses), so re-uploading
a corrected batch updates records instead of duplicating them. Re-uploaded
policy and subnetwork rows that collide with existing names surface as
per-row name-conflict errors rather than duplicates.
## File requirements
* **Format**: Excel workbook (`.xlsx`)
* **Sheet**: only the first sheet is read
* **Row 1**: banner row (ignored)
* **Row 2**: column headers
* **Row 3+**: data rows
See [Workbook Format](/api-overview/sftp/csv-format) for the full rules, including
data types and common pitfalls.
## Relationship to the API channels
SFTP shares everything but the transport with the REST furnishing surface:
* The five directories map one-to-one onto the `slug` values accepted by the
[bulk file upload endpoint](/api-overview/furnishing/file-upload)
(`POST /v1/file-upload/ingest`).
* Data rows go through the same subnetwork routing and policy resolution as a
per-record `POST /v1/products/kyc_certificate/furnish` call — and earn the
same [entitlement](/concepts/governance/entitlement) for your organization.
* The dashboard shows SFTP drops and API uploads in the same uploads view,
with the same row-level result reporting.
That means you can mix channels freely: backfill history over SFTP, furnish
new onboarding events over REST in real time, and the network treats the
resulting data identically.
## Environments
Use plus-addressing on the email username to target an environment:
| Environment | Username format | Example |
| ----------- | -------------------------- | ---------------------------- |
| Production | `email@domain.com` | `alice@yourbank.com` |
| Sandbox | `email+sandbox@domain.com` | `alice+sandbox@yourbank.com` |
Each environment writes to its own isolated storage; nothing uploaded to
sandbox can reach production data. Start in sandbox — the
[getting-started guide](/api-overview/sftp/getting-started) walks through a first
upload end to end.
## Who can upload what
The five categories split into two permission tiers:
* **Data categories** (`kyc_furnish_data`, `kyb_furnish_data`) are for
organizations holding the [furnisher role](/concepts/governance/network-roles)
in the target network.
* **Configuration categories** (`kyc_cert_policy`, `kyb_cert_policy`,
`subnetworks`) define network rules and are enforced as governor-only at
ingest — rows uploaded by a non-governor fail with an access error even
though the file itself uploads successfully.
## In the dashboard
Your first upload, end to end, in about 15 minutes.
Required and optional columns for every category.
Header normalization, data types, and formatting rules.
How SFTP relates to the REST furnishing channels.
# Upload Categories
Source: https://docs.solo.one/api-overview/sftp/schemas
All upload categories available for SFTP ingestion
Each category corresponds to a directory name in the upload path:
```
{category}/{filename}.xlsx
```
The tables below are the column-by-column reference for every category. Column
names are given in **normalized form** — headers in your workbook are matched
after [normalization](/api-overview/sftp/csv-format#header-normalization), so
`KYC Policy Name`, `kyc policy name`, and `kyc_policy_name` are all the same
column, and column order never matters.
Two kinds of "required" apply:
* **Required header** — the column must exist in row 2, or the whole workbook
is rejected.
* **Required value** — the cell must be non-blank on each data row, or that
row is rejected (or skipped, for identifier columns that mark blank rows).
Upload order matters across categories: **policies → subnetworks → data**.
Subnetworks reference policies by name, and data rows reference subnetworks
by name. A reference to something not yet ingested fails at the row level.
***
## KYC Certificate Policy
**Directory**: `kyc_cert_policy/`
Defines Know Your Customer verification policies. Each row configures a named
policy whose operation flags control which verification steps are active.
Policy workbooks configure the network, so they are accepted only from the
network's [governor](/concepts/governance/network-governance).
KYC Certificate Policy Template (.xlsx)
### Required columns
| Column | Type | Value required | Description |
| ----------------- | ------ | -------------- | ---------------------------------------------------------------------------------------------- |
| `kyc_policy_name` | string | Yes | Name of the KYC policy. Rows with a blank name are skipped. Must be unique within the network. |
| `start_date` | date | No | When the policy becomes active. Excel date cell or ISO text. |
| `entity_type` | string | No | When present, must be exactly `Consumer` — any other value rejects the row. |
### Operation flag columns (headers required)
Each flag enables or disables a verification step in the policy. Accepted
values: `true`/`false`, `yes`/`no`, `y`/`n`, `1`/`0` (case-insensitive).
**A blank cell is read as `false`.** All nine headers must be present.
| Column | Description |
| ------------------------ | ---------------------------------- |
| `document_capture` | Require document image capture |
| `document_review` | Require document attribute review |
| `biometric_capture` | Require biometric capture |
| `biometric_review` | Require biometric attribute review |
| `liveness_capture` | Require liveness check capture |
| `liveness_review` | Require liveness evidence review |
| `address_capture` | Require address capture |
| `address_verification` | Require address verification |
| `identity_corroboration` | Require identity corroboration |
### Optional columns
| Column | Type | Description |
| --------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `kyc_policy` | string | Policy number (the template's `KYC Policy` column). Values starting with `Ex.` mark the row as a skipped template example. |
| `end_date_if_applicable` | date | When the policy is deprecated |
| `data_source` | string | Source of verification data |
| `name_of_furnisher_storage_system_used` | string | Storage system identifier |
| `name_of_storage_system_other_only` | string | If storage system is "Other" |
| `entity_consent_level` | string | Level of consumer consent obtained |
| `entity_consent_mechanism_type` | string | How consent was collected |
| `name_of_entity_consent_mechanism_other_only` | string | If consent mechanism is "Other" |
| `permitted_purpose_scope_s` | string | Permitted purpose scopes (template header: `Permitted Purpose Scope(s)`) |
***
## KYB Certificate Policy
**Directory**: `kyb_cert_policy/`
Defines Know Your Business verification policies. Same structure as KYC
policies, with business-specific operation flags. Governor-only, like all
configuration categories.
KYB Certificate Policy Template (.xlsx)
### Required columns
| Column | Type | Value required | Description |
| ----------------- | ------ | -------------- | ----------------------------------------------------------- |
| `kyb_policy_name` | string | Yes | Name of the KYB policy. Rows with a blank name are skipped. |
| `start_date` | date | No | When the policy becomes active |
| `entity_type` | string | No | When present, must be exactly `Business` |
### Operation flag columns (headers required)
Same value rules as the KYC flags (blank reads as `false`):
| Column | Description |
| ----------------------------------------- | ------------------------------------------ |
| `business_identity_verification` | Require business identity verification |
| `business_ownership_control_verification` | Require ownership and control verification |
| `business_risk_compliance_verification` | Require risk and compliance assessment |
The trailing `_verification` may be omitted in these three headers —
`business_identity`, `business_ownership_control`, and
`business_risk_compliance` map to the same flags.
### Optional columns
`kyb_policy` (policy number; `Ex.` prefix marks example rows) plus the same
optional metadata columns as the KYC Certificate Policy category
(`end_date_if_applicable`, data source, storage system, consent fields,
permitted purpose scopes).
***
## Subnetworks
**Directory**: `subnetworks/`
Configures network subnetworks that link KYC and KYB policies together with
effective date windows. One spreadsheet row becomes one subnetwork plus one
subnetwork–policy link per filled slot. The template provides **5 KYC slots and
5 KYB slots** per row. Governor-only.
Subnetworks Template (.xlsx)
### Required columns
| Column | Type | Value required | Description |
| ------------------- | ------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `subnetwork_number` | string | No | Subnetwork identifier. The template header `Subnetwork #` normalizes to `subnetwork`, which is also accepted. Values starting with `Ex.` mark example rows. |
| `subnetwork_name` | string | Yes | Display name of the subnetwork; rows with a blank name are skipped. Must be unique within the network. |
| `kyc_policy_1_name` | string | No | First KYC slot's policy name (header must exist; the slot may be left blank) |
| `kyb_policy_1_name` | string | No | First KYB slot's policy name (header must exist; the slot may be left blank) |
### Policy slot columns
Each slot follows the pattern below, where `{n}` is the slot number (1–5 in
the template):
| Column pattern | Type | Description |
| ---------------------------------- | ------ | ------------------------------------------ |
| `kyc_policy_{n}_name` | string | KYC policy name for slot *n* |
| `kyc_policy_{n}_start` | date | Effective start date for slot *n* |
| `kyc_policy_{n}_end_if_applicable` | date | Effective end date for slot *n* (optional) |
| `kyb_policy_{n}_name` | string | KYB policy name for slot *n* |
| `kyb_policy_{n}_start` | date | Effective start date for slot *n* |
| `kyb_policy_{n}_end_if_applicable` | date | Effective end date for slot *n* (optional) |
Per-slot rules, enforced row by row:
* A slot whose name, start, and end are **all blank** is skipped silently.
* A slot with a **name but no start date** rejects the row.
* A slot with a **date but no name** rejects the row.
* The end date is always optional — leave it blank for open-ended links.
Policy names must match policies already ingested via the KYC or KYB
Certificate Policy categories. Upload policies before subnetworks. These slot
date windows are what later decide which policy applies to a furnished
record's `application_date` — see
[furnishing policies](/concepts/governance/furnishing-policies).
***
## KYC Furnish Data
**Directory**: `kyc_furnish_data/`
Consumer onboarding records furnished toward KYC certificates. Each row is one
consumer tied to a subnetwork; at ingest the row is matched against the
subnetwork's linked policies by `application_date`, and runs once per policy
whose window
covers it (rows outside every window are filtered, not errored).
KYC Furnish Data Template (.xlsx)
### Required columns
| Column | Type | Description |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file` | string | Furnisher file number (the template's `File #` column). Unique row identifier; `Ex.` prefix marks skipped example rows. |
| `social_security_number` | string | Consumer SSN — the upsert key that matches the row to a consumer [entity](/concepts/identity/entities). **Format the column as Text** so leading zeros survive. |
| `date_of_birth` | date | Consumer date of birth |
| `subnetwork_name` | string | Subnetwork this record belongs to (must exist in the network) |
| `application_date` | date | When the consumer applied. Drives policy resolution; time-of-day is not preserved. |
### Optional columns
| Column | Type | Description |
| ------------------------- | ------ | --------------------------------------------------------------------------- |
| `first_name` | string | Consumer first name |
| `last_name` | string | Consumer last name |
| `phone_number` | string | Consumer phone number |
| `email` | string | Consumer personal email address |
| `furnisher_id` | string | Your external identifier for the record, carried through as opaque metadata |
| `furnisher_federation_id` | string | Your external federation identifier, carried through as opaque metadata |
`social_security_number` is a **string**, not a number. An SSN placed in a
numeric Excel cell loses its leading zeros before SOLO ever sees it, which
changes the matching key and can attach the record to the wrong consumer.
***
## KYB Furnish Data
**Directory**: `kyb_furnish_data/`
Business onboarding records furnished toward KYB certificates. Each row is one
business tied to a subnetwork, with the same subnetwork/policy resolution
behavior as KYC data.
KYB Furnish Data Template (.xlsx)
### Required columns
| Column | Type | Description |
| ------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| `file` | string | Furnisher file number (`File #`). Unique row identifier; `Ex.` prefix marks skipped example rows. |
| `business_tax_identifier_value` | string | Tax identifier (e.g. EIN). Read as a string so leading zeros are preserved — format as Text. |
| `business_tax_identifier_type` | string | Identifier kind, e.g. `EIN`. Only `EIN` rows proceed through the pipeline today; rows with other types are filtered out. |
| `business_jurisdiction_of_formation` | string | Jurisdiction where the business is formed — part of the business's matching key alongside the EIN. |
| `subnetwork_name` | string | Subnetwork this record belongs to |
| `application_date` | date | When the business applied; drives policy resolution |
### Optional columns
| Column | Type | Description |
| ----------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `business_legal_name` | string | Official legal name |
| `business_dba_name` | string | Doing-business-as name |
| `business_website_url` | string | Business website |
| `business_registration_identifier_from_jurisdiction_of_formation` | string | Registration number from the jurisdiction of formation |
| `identity_verification_timestamp` | date | When business identity verification was performed (header `identity_verification_ts` also accepted) |
| `ownership_control_verification_timestamp` | date | When ownership/control verification was performed (`ownership_control_verification_ts` also accepted) |
| `risk_compliance_assessment_timestamp` | date | When the risk/compliance assessment was performed (`risk_compliance_ts` also accepted) |
| `furnisher_id` | string | Your external identifier, carried through as opaque metadata |
| `furnisher_federation_id` | string | Your external federation identifier, carried through as opaque metadata |
When the three verification timestamps are omitted, the furnish pipeline
falls back to the calendar date of `application_date` for each one. Provide
them when you have the real dates — they become the evidence timestamps on
the resulting certificates.
***
## Quick reference
| Category | Identifier column (`Ex.` detector) | Row key for upserts | Who may upload |
| ------------------ | ---------------------------------- | ---------------------------------------------------------------------- | -------------- |
| `kyc_cert_policy` | `kyc_policy` (Policy #) | Policy name (conflicts error per row) | Governor |
| `kyb_cert_policy` | `kyb_policy` (Policy #) | Policy name (conflicts error per row) | Governor |
| `subnetworks` | `subnetwork` (`Subnetwork #`) | Subnetwork name (conflicts error per row) | Governor |
| `kyc_furnish_data` | `file` (`File #`) | `social_security_number` | Furnisher |
| `kyb_furnish_data` | `file` (`File #`) | `business_tax_identifier_value` + `business_jurisdiction_of_formation` | Furnisher |
For layout, header normalization, and cell-type rules shared by every
category, see [Workbook Format](/api-overview/sftp/csv-format).
# Query the bank-specific bad actor list for a consumer or business
Source: https://docs.solo.one/api-reference/bank-specific-bad-actor-list/query
POST /v1/products/bank_specific_bad_actor_list/query
# Create a business consent record (direct, server-to-server). Returns a consent_id used for all subsequent queries.
Source: https://docs.solo.one/api-reference/business/business
POST /v1/consent/business
Create a business consent record (direct, server-to-server). Returns a consent_id used for all subsequent queries.
# Create a consumer consent record (direct, server-to-server). Returns a consent_id used for all subsequent queries.
Source: https://docs.solo.one/api-reference/consumer/consumer
POST /v1/consent/consumer
Create a consumer consent record (direct, server-to-server). Returns a consent_id used for all subsequent queries.
# Update a consumer consent record's scope, expiry, or consented fields
Source: https://docs.solo.one/api-reference/consumer/update
PUT /v1/consent/consumer/{consent_id}
Update a consumer consent record's scope, expiry, or consented fields. Identity fields are immutable.
# Read a consumer consent record by consent_id.
Source: https://docs.solo.one/api-reference/consumer/{consent_id}
GET /v1/consent/consumer/{consent_id}
Read a consumer consent record by consent_id.
# Query the cross-bank financial crimes watch list for a consumer or business
Source: https://docs.solo.one/api-reference/cross-bank-financial-crimes-watch-list/query
POST /v1/products/cross_bank_financial_crimes_watch_list/query
# Search businesses by name, EIN, or email.
Source: https://docs.solo.one/api-reference/entities/business-search
GET /v1/entities/business/search
Search business core identities using partial or exact field matches.
Parameters
----
`network_id` : `UUID`.
Network scope for the search.
`business_legal_name` : `str`, optional.
Case-insensitive partial match on legal name.
`business_dba_name` : `str`, optional.
Case-insensitive partial match on DBA name.
`business_email` : `str`, optional.
Case-insensitive partial match on email.
`federal_ein` : `int`, optional.
Exact match on federal EIN.
`limit` : `int`.
Maximum number of results (1-100). Defaults to 20.
Returns
----
`results` : `list[BusinessSearchResult]`.
Matching business core identities.
Examples
-----
```
GET /entities/business/search?network_id=...&business_legal_name=Acme&limit=10
```
# Search consumers by name, email, SSN, or date of birth.
Source: https://docs.solo.one/api-reference/entities/consumer-search
GET /v1/entities/consumer/search
Search consumer core identities using partial or exact field matches.
Parameters
----
`network_id` : `UUID`.
Network scope for the search.
`first_name` : `str`, optional.
Case-insensitive partial match on first name.
`last_name` : `str`, optional.
Case-insensitive partial match on last name.
`personal_email` : `str`, optional.
Case-insensitive partial match on email.
`social_security_number` : `str`, optional.
Exact match on SSN.
`date_of_birth` : `str`, optional.
Exact match on date of birth (``YYYY-MM-DD``).
`limit` : `int`.
Maximum number of results (1-100). Defaults to 20.
Returns
----
`results` : `list[ConsumerSearchResult]`.
Matching consumer core identities.
Examples
-----
```
GET /entities/consumer/search?network_id=...&first_name=John&last_name=Doe&limit=10
```
# Upload a CSV file for ingestion
Source: https://docs.solo.one/api-reference/file-upload/ingest
POST /v1/file-upload/ingest
Upload a CSV file and queue it for ingestion.
The file is accepted synchronously and processed asynchronously. The
response contains a record describing the upload; its ``status`` begins
as ``pending`` and transitions to ``processing`` and then ``completed``
(or ``failed``) as ingestion proceeds.
Parameters
----
`file` : multipart file.
The CSV file to upload.
`slug` : `str`.
The data category for this upload (e.g. ``kyc_cert_policy``,
``kyb_cert_policy``). Must be one of the categories supported for
your account.
Returns
----
`file_upload` : `FileUpload`.
The created upload record, including its ``id`` and current
``status``.
Raises
----
`ValidationError`.
If the file name or ``slug`` is invalid.
Examples
-----
```bash
>>> curl -X POST https://app.solo.one/file-upload/ingest \
... -H "Authorization: Bearer $TOKEN" \
... -F "file=@sample.csv" \
... -F "slug=kyc_cert_policy"
```
# Configure a furnishing policy
Source: https://docs.solo.one/api-reference/furnishing-policies/configuration
PUT /v1/networks/policies/furnishing/{policy_id}/configuration
# Create a furnishing policy
Source: https://docs.solo.one/api-reference/furnishing-policies/create
POST /v1/networks/policies/furnishing
# List furnishing policies in a network.
Source: https://docs.solo.one/api-reference/furnishing-policies/furnishing
GET /v1/networks/policies/furnishing
List furnishing policies visible within a network scope.
Results are paginated with ``limit`` (1-100, default 20) and
``offset`` (>=0, default 0).
Parameters
----
`network_id` : `UUID`.
Network scope for the listing.
`limit` : `int`.
Maximum number of results. Defaults to 20.
`offset` : `int`.
Number of results to skip. Defaults to 0.
Returns
----
`results` : `list[FurnishingPolicyResult]`.
Page of matching furnishing policies.
Examples
-----
```
GET /networks/policies/furnishing?network_id=...&limit=10
```
# Get a furnishing policy by id.
Source: https://docs.solo.one/api-reference/furnishing-policies/{policy_id}
GET /v1/networks/policies/furnishing/{policy_id}
Fetch a single furnishing policy by id, scoped to a network.
Parameters
----
`policy_id` : `UUID`.
Identifier of the furnishing policy to fetch.
`network_id` : `UUID`.
Network scope for the lookup.
Returns
----
`result` : `FurnishingPolicyResult`.
The matching furnishing policy.
Raises
----
`NotFoundError`.
If no furnishing policy with the given id is visible within the
given network scope.
Examples
-----
```
GET /networks/policies/furnishing/{policy_id}?network_id=...
```
# Create or renew an institution attestation for a product.
Source: https://docs.solo.one/api-reference/institution-attestation/institution-attestation
POST /v1/institution-attestation
Create a new institution-level attestation scoped to the caller's entity and product.
The attestation is valid for 12 months from the time it is created.
Creating a new attestation does not revoke prior ones; the most
recently created active attestation for a given product is treated
as the current one.
Parameters
----------
`body` : `InstitutionAttestationCreateRequest`.
Attestation payload.
Returns
-------
`response` : `InstitutionAttestationResponse`.
The newly created attestation record.
# API Reference
Source: https://docs.solo.one/api-reference/introduction
SOLO Network API endpoint reference
The SOLO Network API reference is auto-generated from the
[OpenAPI specification](/api-reference/openapi.json). Each endpoint includes
request/response schemas, parameter details, and example payloads.
New to the platform? Read [High-level concepts](/home/overview) first — the
reference assumes familiarity with entities, products, policies, networks,
consent, and entitlement.
## Authentication
All endpoints are authenticated with a Bearer token. See
[Authentication](/home/authentication).
```
Authorization: Bearer
```
## How the reference is organized
Create and read [consent](/concepts/identity/consent) records for consumers and
businesses. Required before querying.
Search consumers and businesses within a [network](/concepts/governance/networks).
Query and furnish [products](/api-overview/querying/products) — KYC/KYB certificates and
screening lists.
Create and configure querying [policies](/concepts/governance/querying-policies), and list
furnishing policies.
Create or renew an institution attestation for a product.
Upload files for ingestion. See also the [SFTP guide](/api-overview/sftp/overview).
## Conventions
* **Products** expose `query` (read) and `furnish` (write) operations under
`/v1/products/{product}/…`.
* **Querying** a product requires a `consent_id` and one or more `network_policy`
pairs. See [Querying](/api-overview/querying/overview).
* **Furnishing** a product requires a `network_id`, `subnetwork_name`, and
`application_date`. See [Furnishing](/api-overview/furnishing/overview).
* Errors follow a consistent JSON shape — see [Errors](/home/errors).
# Furnish KYB certificate data via structured JSON
Source: https://docs.solo.one/api-reference/kyb-certificate/furnish
POST /v1/products/kyb_certificate/furnish
# Query consolidated KYB certificate data for a business
Source: https://docs.solo.one/api-reference/kyb-certificate/query
POST /v1/products/kyb_certificate/query
# Furnish KYC certificate data via structured JSON
Source: https://docs.solo.one/api-reference/kyc-certificate/furnish
POST /v1/products/kyc_certificate/furnish
# Query consolidated KYC certificate data for a consumer
Source: https://docs.solo.one/api-reference/kyc-certificate/query
POST /v1/products/kyc_certificate/query
# Check per-furnisher field coverage before running a query
Source: https://docs.solo.one/api-reference/products/check
POST /v1/products/check
# Save per-field selections onto an existing querying policy.
Source: https://docs.solo.one/api-reference/querying-policies/configuration
PUT /v1/networks/policies/querying/{policy_id}/configuration
Persist per-field selections onto an existing querying policy.
Replaces the policy's per-model and per-field selections with the
submitted set. The policy is left in ``published`` status.
# Create a querying policy for a (product, network) pair.
Source: https://docs.solo.one/api-reference/querying-policies/querying
POST /v1/networks/policies/querying
Create a new querying policy for a (product, network) pair.
The ``(network_id, product_id)`` pair is resolved server-side to its
network-product association (auto-creating it if missing). Per-model
and per-field selections for the policy are authored separately via
``PUT /networks/policies/querying/{policy_id}/configuration``.
# Entitlement
Source: https://docs.solo.one/concepts/governance/entitlement
Why you can read a given field about an entity
**Entitlement** is what determines *which data* you may read about an entity.
It is the third and most granular layer of SOLO's access model:
| Layer | Grants | Question it answers |
| -------------------------------------------------------- | -------------- | ---------------------------------------------------------------------- |
| [Network membership](/concepts/governance/network-roles) | **Capability** | *May I call this kind of operation on this network at all?* |
| [Consent](/concepts/identity/consent) | **Permission** | *May I query this particular subject?* |
| **Entitlement** | **Visibility** | *Of the data that exists, which records and fields may* ***I*** *see?* |
Membership lets you ask; consent makes the question lawful; entitlement decides
what the answer contains.
The principle is simple: **you can read what you have contributed or previously
looked at.** You earn entitlement to an entity's data by participating in the
network — not by your role, and not by network membership alone.
```mermaid theme={null}
flowchart LR
F[You furnish an entity] --> Ent[Entitlement ledger]
Q[You query an entity] --> Ent
G[A field access grant
is created with consent] --> Ent
Ent --> Read[Readable data on future queries]
```
## Where entitlement comes from
SOLO maintains, per participant, a ledger of which records that participant has
earned the right to read. Entries land in the ledger three ways.
### Furnishing
When you **furnish** data for an entity, SOLO records an entitlement link for
each record you contributed, tied to your entity and the subject's profile. On
future queries for that subject, those records — and the consolidated results
built from them — are readable by you.
This is the most direct path: furnish what you know about a consumer or
business, and subsequent queries for that subject return data — yours, plus
anything else you're entitled to — consolidated into one result.
### Querying
When you **query** an entity (with valid
[consent](/concepts/identity/consent)), SOLO records entitlement links for the
records that query actually read. Having lawfully queried a subject, you remain
entitled to the data that query covered.
Entitlement therefore accrues over time. Each legitimate interaction —
furnishing or querying — widens the set of data you can see for the entities
you've actually engaged with. At read time, SOLO takes the **union** of your
furnish-derived and query-derived entitlements.
### Field access grants
The third source is explicit: a **field access grant**. A grant ties a
[consent](/concepts/identity/consent) record to a *furnishing entity* and
enumerates the specific field definitions you may read from that furnisher's
data about the subject.
Grants are created automatically when a consent record is created, and they are
returned when you read the consent back:
```json theme={null}
{
"consent_id": "a3f0b9c7-…",
"field_access_grants": [
{
"furnishing_entity_id": "7c2d91e4-…",
"field_definitions": ["consumer.first_name", "consumer.last_name"],
"effective_from": "2026-06-01",
"effective_to": null
}
]
}
```
Three properties of grants matter operationally:
* **Effective windows.** A grant carries `effective_from` and `effective_to`
dates. A grant outside its window contributes nothing.
* **Revocation.** A revoked grant stops contributing immediately, regardless of
its window.
* **Consent expiry.** Grants ride on the consent they belong to — when the
consent expires, its grants stop expanding your access.
While a grant is active, reads on the subject can draw on the named furnishing
entity's data for the granted fields — even data you didn't furnish and hadn't
previously read.
## How entitlement is enforced
Entitlement is enforced at **read time, per record**. When you run a query,
SOLO loads your ledger for the subject — the union of everything you've earned
through furnishing and through prior queries — and filters the candidate
records down to the ones the ledger covers. Records you aren't entitled to are
simply not consulted when the consolidated result is built; they don't appear
redacted, they don't appear at all.
```mermaid theme={null}
sequenceDiagram
participant You as Your query
participant SOLO as SOLO
participant Ledger as Entitlement ledger
participant Data as Network data
You->>SOLO: query subject (consent_id, network_ids)
SOLO->>Ledger: load your entitlements for this subject
Ledger-->>SOLO: records earned via furnish + query + grants
SOLO->>Data: read only entitled records
Data-->>SOLO: rows
SOLO-->>You: consolidated result (policy-capped)
```
Three consequences follow from this design:
* **Entitlement is per subject.** Furnishing one consumer earns you nothing
about a different consumer, even one furnished by the same institution into
the same network.
* **Entitlement is per participant.** It attaches to *your* entity. It is not
shared across a network, not inherited from a parent network, and not
transferred when roles change hands.
* **Entitlement doesn't expire on its own.** Furnish- and query-derived
entitlements persist; only field access grants carry effective windows and
revocation. (Consent, by contrast, can expire — and you always need valid
consent to ask in the first place.)
## Putting it together
A query result is shaped by three layers, applied in order:
You hold a valid [consent](/concepts/identity/consent) record for the
subject — you're permitted to ask.
The network's [querying policy](/concepts/governance/querying-policies)
defines the maximum set of fields this product can expose in this network —
for anyone.
Of those fields, you receive only what you've **earned** — through prior
furnishing or querying of this entity, or through an active field access
grant.
The result is the **intersection** of what the policy allows and what you're
entitled to:
| Question | Answered by |
| ----------------------------------------------------------------- | ---------------------------------------- |
| *In this network, what is this product allowed to expose at all?* | **Querying policy** — what *may* be read |
| *Of that, what have I personally earned the right to read?* | **Entitlement** — what *you* may read |
This is why two participants can run the *same* query on the *same* entity
and get *different* results: each sees only the data they've earned access
to. The policy is shared; the entitlement ledger is per-participant.
## Worked examples
The JSON below is illustrative sketches, trimmed to the fields that matter for
each example.
### Example A: a furnisher reads back its own furnished fields
Coastal Bank furnishes a KYC record for Jane Doe into its network:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/furnish \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "coastal-cards",
"application_date": "2026-05-28",
"records": [{ "first_name": "Jane", "last_name": "Doe", "date_of_birth": "1990-01-15", "social_security_number": "123-45-6789" }]
}'
```
The furnish creates entitlement links between Coastal and every record it
contributed. When Coastal later queries Jane (with consent), the consolidated
result includes the data Coastal furnished — no extra setup, no grant needed:
```json theme={null}
{
"results": [{
"meta": { "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" },
"document_capture": { "…": "populated from Coastal's furnished records" },
"biometric_capture": { "…": "populated" }
}]
}
```
### Example B: a querier sees gaps because it lacks entitlement
Summit Bank — a member of the same network, with the querier role and a valid
consent for Jane — runs the *identical* query. Summit has never furnished or
queried Jane, and no grant names Summit. The query is **authorized** (membership
* consent), and the policy would allow the fields — but Summit's entitlement
ledger has nothing for Jane:
```json theme={null}
{
"results": [{
"meta": { "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" },
"document_capture": null,
"biometric_capture": null
}]
}
```
Sections come back empty rather than erroring — an entitlement gap looks like
*missing data*, not like a `403`. For certificate products, if the entitled
data can't satisfy the policy's requirements at all, the API returns **`204 No
Content`** instead of issuing a certificate.
### Example C: a field access grant expands the result
Jane now consents to Summit's inquiry, and the consent's field access grant
names Coastal as the furnishing entity whose data Summit may read:
```json theme={null}
{
"consent_id": "b8e1d2f0-…",
"field_access_grants": [{
"furnishing_entity_id": "",
"field_definitions": ["consumer.first_name", "consumer.last_name", "consumer.date_of_birth"],
"effective_from": "2026-06-01",
"effective_to": "2027-06-01"
}]
}
```
Summit re-runs the same query — before and after:
```json theme={null}
{
"results": [{
"meta": { "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" },
"document_capture": null
}]
}
```
```json theme={null}
{
"results": [{
"meta": { "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" },
"document_capture": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15"
}
}]
}
```
The grant widened Summit's visibility into Coastal's furnished data for the
granted fields, for the grant's effective window. And because Summit has now
lawfully *queried* Jane, those reads are themselves recorded — Summit stays
entitled to what this query covered, even after the grant's window closes.
## What entitlement is not
* **Not a role.** Being a
[governor](/concepts/governance/networks#permissions--roles) of a network does
not entitle you to members' data.
* **Not network containment.** Sharing a network with another participant does
not entitle you to what they furnished.
* **Not retroactive by configuration.** Changing roles, network settings, or
[governance rules](/concepts/governance/network-governance) does not grant
access to data you didn't earn through your own furnishing, querying, or an
explicit grant.
* **Not a bypass of policy.** An entitlement (or grant) to a field the
[querying policy](/concepts/governance/querying-policies) excludes still
returns nothing — the policy is the ceiling.
## Troubleshooting: "why is this field missing?"
Work down this checklist when a query succeeds but a field you expected is
absent:
1. **Is the field enabled in the querying policy?** The policy caps every
reader. Check the policy's field configuration — see
[Querying Policies](/concepts/governance/querying-policies).
2. **Have you furnished this subject?** If not, you have no furnish-derived
entitlement to their records.
3. **Have you previously queried this subject?** Query-derived entitlement only
covers what earlier queries actually returned.
4. **Is there a field access grant for you — and is it alive?** Check the
consent record's `field_access_grants`: is the field listed in
`field_definitions`, is today inside `effective_from`/`effective_to`, has the
grant been revoked, has the consent expired?
5. **Was the data furnished at all?** Entitlement filters existing data; it
can't surface a field nobody contributed. A
[coverage check](/api-overview/querying/coverage-check) tells you whether data
exists before you spend a query.
6. **Are you querying the right network scope?** Data is network-bounded; see
[Network Governance](/concepts/governance/network-governance) for which
networks your query can reach.
If all six check out and the field is still missing, contact your SOLO account
manager with the `request_id` from the response.
## Related concepts
The permission layer that gates whether you may query at all — and carries
field access grants.
The per-network ceiling on what a product can expose.
How entitlement is applied when a query runs.
Contributing data — the most direct way to earn entitlement.
# Furnishing Policies
Source: https://docs.solo.one/concepts/governance/furnishing-policies
How a network accepts and routes furnished data for a product
A **furnishing policy** defines how furnished data for a
[product](/api-overview/querying/products) is **accepted, validated, and routed**
into a [network](/concepts/governance/networks). Where a
[querying policy](/concepts/governance/querying-policies) governs what leaves
the network on a read, a furnishing policy governs what happens to data on the
way in.
As a furnisher you never pass a furnishing policy yourself. You
[furnish](/api-overview/furnishing/overview) with a network, a subnetwork, and an
application date — and the network resolves the applicable policy
automatically. Furnishing policies are authored and managed by the network's
[governor](/concepts/governance/network-roles).
```mermaid theme={null}
flowchart LR
F[Furnish request
network + subnetwork + application_date] --> R{Policy resolution}
R --> P[Furnishing policy]
P --> S1[Step 1: validate]
S1 --> S2[Step 2: match entity]
S2 --> S3[Step 3: persist + route]
S3 --> Store[(Network data)]
```
## The pipeline model
Under the hood, a furnishing policy is a **pipeline**: an ordered sequence of
processing steps that every furnished record runs through. The pipeline for a
policy is derived from a **product default** — a SOLO-maintained template
pipeline for the product, identified by the policy's `source_default_slug`
(resolved from the product's catalogue path when the policy is created).
A policy customizes the default at three levels, which is exactly the surface
the configuration API exposes:
| Configuration | What it controls |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled_step_names` | Which of the default pipeline's steps run for this policy. Some steps are mandatory and are always retained, whether or not they are listed. |
| `step_model_defaults` | Per-step default values applied to the models a step processes. |
| `step_field_configuration` | Per-step, per-field settings — the field-level knobs a step exposes. |
Alongside the steps, a policy carries optional **runtime context** (named
inputs resolved when the pipeline runs) and an optional **filter** that rejects
records up front — this is what enforces the application-date windows described
below. You don't author these directly through the REST API; they come from
the product default and from workbook-based authoring.
The step names and their available settings are product-specific — they come
from the product's default pipeline. List an existing policy (below) or
consult your SOLO account manager for the step catalogue of the products you
operate.
## Policy fields and versioning
| Field | Meaning |
| --------------------- | ------------------------------------------------------------------------------ |
| `name` | Display name. Together with `network_id` and `version`, must be unique. |
| `version` | Integer version, starting at 1. New revisions of the same policy increment it. |
| `schema_version` | Version of the pipeline document schema the policy conforms to. |
| `source_default_slug` | Which product default pipeline this policy is derived from. |
| `active_on` | When the policy takes effect. **Once set, the policy is immutable.** |
| `deprecated_on` | When the policy is retired. Resolution skips deprecated policies. |
| `policy_metadata` | Free-form metadata recorded by the author. |
Versioning follows a simple rule: **activated policies never change**. While a
policy is a draft (`active_on` unset), its configuration can be replaced
freely. Setting `active_on` freezes it; further configuration writes are
rejected:
```json theme={null}
{
"detail": "Activated furnishing policies are immutable; create a new version",
"error_code": "VALIDATION_ERROR"
}
```
To evolve an active policy, create a new version with the same `name` and a
higher `version`, configure it, and activate it. When SOLO needs "the current
policy named X in network Y", it picks the **highest-version, non-deprecated**
one.
A typical revision cycle looks like this:
```mermaid theme={null}
flowchart LR
D[Draft
configuration replaceable] -->|set active_on| A[Active
immutable]
A -->|set deprecated_on| R[Retired
skipped by resolution]
A -.->|need changes?| D2[New draft
same name, version + 1]
D2 -->|activate| A2[Active v2]
```
Because activation freezes a policy, every furnish is reproducible after the
fact: the policy version that processed a record can be read back exactly as
it was when the record came in. Deprecating the old version (rather than
deleting it) keeps that audit trail intact while taking it out of resolution.
## How a policy is resolved at furnish time
Resolution is automatic and happens on every furnish. Three inputs drive it —
all from the furnish request:
```json theme={null}
{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "coastal-cards",
"application_date": "2026-05-28",
"records": [ { "…": "…" } ]
}
```
`subnetwork_name` is matched to a [subnetwork](/concepts/governance/networks#standard-networks-and-subnetworks)
under `network_id`. An unknown subnetwork is an error — subnetworks are
introduced ahead of time by the governor.
Policies are **assigned** to subnetworks, each assignment carrying an
effective-date window (`effective_from`, optional `effective_to`). Every
non-deprecated policy assigned to the subnetwork for the product is a
candidate.
The record's `application_date` is checked against the policy and
assignment windows. Records that fall outside every window are
**filtered** — not failed — so a furnish can legitimately result in
"accepted under policy v2, filtered by policy v1".
This is why furnishers never reference policies directly: the
(network, subnetwork, application date) triple is sufficient, and the governor
controls what it maps to.
## Managing policies via the API
Five routes cover the lifecycle. Listing and reading are available to any
member of the network; creating and configuring are governor operations.
### List policies in a network
```bash theme={null}
curl -G https://api.solo.one/v1/networks/policies/furnishing \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…" \
--data-urlencode "limit=20" \
--data-urlencode "offset=0"
```
`limit` accepts 1–100 (default 20); `offset` ≥ 0 (default 0). Returns an array:
```json theme={null}
[
{
"id": "4f6e2a90-…",
"entity_id": "7c2d91e4-…",
"network_id": "9f1c0c2e-…",
"name": "Coastal KYC intake",
"version": 2,
"schema_version": "1",
"source_default_slug": "kyc_certificate",
"active_on": "2026-01-01T00:00:00Z",
"deprecated_on": null,
"policy_metadata": {},
"created_at": "2025-12-12T18:03:11Z",
"updated_at": "2025-12-30T09:41:00Z"
}
]
```
### Read a single policy
```bash theme={null}
curl -G https://api.solo.one/v1/networks/policies/furnishing/4f6e2a90-… \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…"
```
The `network_id` query parameter scopes the lookup; a policy that exists but
isn't visible in that network returns `404` `RESOURCE_NOT_FOUND`.
### Create a policy
`POST /v1/networks/policies/furnishing` creates an empty policy **shell** for a
(product, network) pair. The server derives `source_default_slug` from the
product; pipeline steps are added afterwards via configuration (or workbook
ingestion).
```bash theme={null}
curl -X POST https://api.solo.one/v1/networks/policies/furnishing \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "1b9a4c33-…",
"network_id": "9f1c0c2e-…",
"name": "Coastal KYC intake"
}'
```
```json theme={null}
{
"id": "4f6e2a90-…",
"entity_id": "7c2d91e4-…",
"network_id": "9f1c0c2e-…",
"name": "Coastal KYC intake",
"source_default_slug": "kyc_certificate"
}
```
### Configure a policy
`PUT /v1/networks/policies/furnishing/{policy_id}/configuration` replaces the
draft policy's pipeline configuration. `enabled_step_names` is required; the
rest is optional.
```bash theme={null}
curl -X PUT https://api.solo.one/v1/networks/policies/furnishing/4f6e2a90-…/configuration \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled_step_names": ["validate_identity", "match_entity", "persist_records"],
"step_model_defaults": { "validate_identity": { "strictness": "high" } },
"step_field_configuration": { "match_entity": { "match_on": ["ssn", "dob"] } },
"active_on": "2026-07-01T00:00:00Z"
}'
```
```json theme={null}
{
"id": "4f6e2a90-…",
"name": "Coastal KYC intake",
"step_count": 3
}
```
Like querying-policy configuration, this is a **replace**, not a patch — submit
the full intended step selection each time. Step names shown above are
illustrative; use the names from your product's default pipeline. Including
`active_on` activates the policy, after which it is immutable; `deprecated_on`
retires it from resolution.
## Bulk authoring and ingestion
Furnishing policies sit at the heart of SOLO's bulk-ingestion paths:
* **Policy workbooks.** Governors can author policies in bulk by uploading a
policy workbook (per product) through
[file upload](/api-overview/furnishing/file-upload) or
[SFTP](/api-overview/sftp/overview). Each row clones the product default and
applies the row's step selections — equivalent to the create + configure flow
above. Workbook ingestion is rejected when the caller doesn't govern the
target network.
* **Subnetwork workbooks** assign policies (by name, latest non-deprecated
version) to the subnetworks they define.
* **Data files.** Records arriving in bulk — see
[CSV format](/api-overview/sftp/csv-format) and [schemas](/api-overview/sftp/schemas) —
go through exactly the same resolution and pipeline as API furnishes. There is
one set of rules, regardless of transport.
## Relationship to other concepts
| Concept | Relationship |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [Querying policies](/concepts/governance/querying-policies) | The read-side counterpart. A product in a network typically has both: a furnishing policy shaping intake, a querying policy capping reads. |
| [Entitlement](/concepts/governance/entitlement) | Furnishing under a policy is what *earns* a furnisher read access to the subjects it contributed. |
| [Networks & subnetworks](/concepts/governance/networks) | Policies belong to a network; assignments bind them to subnetworks. |
| [Furnishing](/api-overview/furnishing/overview) | The furnisher-facing view: furnish with network + subnetwork + application date, and the policy applies itself. |
## In the dashboard
## Related concepts
The furnisher's view of the same flow.
Field-level read rules — the other half of policy governance.
Bulk delivery of subnetworks, policies, and data.
Inline workbook and CSV ingestion.
# Network Governance
Source: https://docs.solo.one/concepts/governance/network-governance
How query access flows between related networks
**Network governance** is the set of rules that decide whether a querier in one network is allowed to read data scoped to another. Governance sits on top of the network tree (see [Networks](/concepts/governance/networks)) and the role model (see [Network Roles](/concepts/governance/network-roles)).
The model has two parts: a small set of paths that are **always allowed**, and a small set of **opt-in rules** that a network's governor can turn on to permit further query paths.
## How these rules work: authorization vs data visibility
Every governance check answers two questions, in order:
1. **Authorization** — *can* this querier make this query at all? The answer is yes or no. If no, the API returns an authorization error and no data comes back.
2. **Data visibility** — *given* the query is allowed, *what records* come back? The answer is a set, which may be large, small, or empty.
Most of the rules described on this page are about authorization — they decide whether a query is allowed. One rule, `auto_include_descendants`, is purely about visibility — it widens what comes back once a query is already allowed, but it can never authorize a query on its own. The decision matrix at the end of this page is the authorization view; visibility behavior is called out inline with each rule.
This distinction matters in practice. An authorization failure surfaces as a clear error; a visibility filter that excludes all rows surfaces as a successful query with zero results.
## Querier role is the gate
Throughout this page, "querier on N" means an entity that participates on network N with the **querier** role. Running a product query is a querier's job — see [Network Roles](/concepts/governance/network-roles) — so the governance checks below are described from the querier's point of view: can a querier on one network read data scoped to another?
Two clarifications keep this honest:
* **Governance authorizes scope, not content.** Passing a governance check means the query is allowed to *target* the network. What actually comes back is still filtered by [entitlement](/concepts/governance/entitlement) and capped by the [querying policy](/concepts/governance/querying-policies) — a governor or furnisher who is also a member does not, by virtue of those seats, see other members' data.
* **The authorization check consults network membership.** SOLO's scope check asks which networks the calling entity participates in. Holding the querier role is the operational prerequisite for product queries; treat the role model as the contract even where the scope check is membership-based today.
## Always-allowed query paths
These hold for every network. They are built into the model — there is no setting that controls them, and they cannot be turned off.
| If the querying entity is… | …it can query data scoped to… |
| ---------------------------------------------- | ---------------------------------------- |
| A querier on network **N** | network **N** |
| A querier on any **ancestor** of network **N** | network **N** ("query your descendants") |
The intuition: a bank that holds the querier role on a network can always see data in any subnetwork it runs underneath that network, even if it doesn't separately hold the querier role on each subnetwork. A querier in a specific subnetwork can always see that subnetwork's data.
Everything else — querying upward, querying across to a cousin — is **denied by default** unless one of the rules below is explicitly enabled.
## Opt-in rules
Each opt-in rule is a setting **stored on a specific network** — the network whose data is being opened up. When SOLO evaluates a governance check, it reads the rule from that network and nowhere else. Turning a rule on at one network has no effect on any other network, including its ancestors, descendants, or cousins.
A network's governor can enable any of the following three rules.
### `ancestor_query` — let descendants query upward
**Type:** Authorization rule.
**Stored on:** the ancestor network (the one being opened up).
When this rule is enabled on a network, a querier on any **descendant** of that network is authorized to query data scoped to the network itself.
**Example:** Coastal Bank turns on `ancestor_query` on its own network. A querier in *Coastal — Credit Cards* (a subnetwork underneath Coastal Bank) can now query *Coastal Bank* data directly.
### `cousin` — let cousins query each other
**Type:** Authorization rule.
**Stored on:** the **closest** common ancestor of the two cousin networks.
When this rule is enabled, a querier on any network underneath that ancestor is authorized to query data scoped to any other network underneath the same ancestor — even though neither is in the other's direct line.
**Example:** A Federation root turns on `cousin` on its own network. A querier in *Coastal Bank* can now query *Summit Bank* data, because both networks share the Federation as their closest common ancestor.
The rule must sit on the *closest* common ancestor. Setting `cousin` on a higher ancestor — say, an ancestor of the Federation — has no effect on cousins underneath the Federation. Each cousin pair consults exactly one network's setting: their closest shared ancestor.
### `auto_include_descendants` — expand a query to cover descendants
**Type:** Visibility rule.
**Stored on:** the parent network whose descendants should be pulled into queries against it.
This rule does not change *who* can query — it changes *what comes back*. When enabled, a query scoped to a network automatically pulls in data from all of its descendants as well.
**Example:** Coastal Bank turns on `auto_include_descendants` on its own network. A querier authorized to read *Coastal Bank* data automatically gets data from *Coastal — Credit Cards*, *Coastal — Mortgages*, and any other subnetwork underneath Coastal in the same query — without having to query each subnetwork separately.
`auto_include_descendants` is a *scope expander*, not an *authorization*. The querier still needs to be separately authorized to query the parent network — via a direct querier role, a querier role on an ancestor, or one of the authorization rules above.
**No descendant membership required.** When `auto_include_descendants` is enabled, the descendants' data flows into the parent query automatically — the querier does not need to hold a querier role on each descendant network. This is what makes it a visibility rule rather than an authorization rule: a senior network's governor can declare "anyone authorized to query me can also see what flows through my children," without each querier separately enrolling on every descendant.
## Putting it together: the authorization matrix
Here's the complete authorization matrix for whether a querier can read data scoped to a given network. The matrix covers **only the authorization question** — the yes/no decision about whether the query is allowed at all. Once a query is authorized, `auto_include_descendants` may additionally widen what comes back; that visibility behavior is separate from the matrix below.
The third column shows **which network holds the setting** SOLO consults — that is, which governor controls the answer.
| Querier's network in relation to the target | Authorized? | Where the controlling rule lives |
| ------------------------------------------- | ----------------------------------- | -------------------------------- |
| Querier on the target | Always | — (built in, no rule) |
| Querier on an ancestor of the target | Always | — (built in, no rule) |
| Querier on a descendant of the target | Only if `ancestor_query` is enabled | On the target network |
| Querier on a cousin of the target | Only if `cousin` is enabled | On the closest common ancestor |
| Querier with no relationship to the target | Never | — (no rule grants this) |
## Worked example 1: a sponsor bank and its fintech subnetworks
Harbor Bank sponsors two fintechs. Each fintech's offering runs as a **subnetwork** under Harbor's network:
```mermaid theme={null}
graph TD
Harbor[Harbor Bank — standard network]
Lendly[Lendly — Personal Loans subnetwork]
CardCo[CardCo — Charge Cards subnetwork]
Harbor --> Lendly
Harbor --> CardCo
```
Harbor holds querier (and governor) on its own network. Lendly and CardCo each hold furnisher and querier on **their own subnetwork only**.
**What works out of the box:**
* **Lendly queries Lendly.** A querier on a network can always query it. Same for CardCo.
* **Harbor queries either subnetwork.** Harbor is an ancestor of both, and "query your descendants" is built in. Harbor does not need a seat on each subnetwork.
**What is denied by default:**
* **Lendly queries Harbor.** That's querying *upward*. It stays denied until Harbor's governor enables `ancestor_query` **on Harbor's own network** — Lendly's governor cannot flip that switch.
* **Lendly queries CardCo.** They are cousins (siblings, sharing Harbor as parent). Denied until Harbor — their closest common ancestor — enables `cousin`. Most sponsors deliberately leave this off: it's exactly the wall that keeps one fintech's customers invisible to another.
**One convenience Harbor will likely want:** enabling `auto_include_descendants` on Harbor's network means a single Harbor-scoped query also returns data flowing through *Lendly* and *CardCo*, without Harbor issuing one query per subnetwork. This is a visibility rule — it widens Harbor's already-authorized query; it grants nothing to the fintechs.
Even with every rule enabled, [entitlement](/concepts/governance/entitlement) still applies: Harbor sees consolidated results only for the subjects it has furnished or lawfully queried, and the fields are capped by the [querying policy](/concepts/governance/querying-policies). Governance opens the door to the room; it doesn't hand over the filing cabinet.
## Worked example 2: two cousin banks in a federation
Coastal Bank and Summit Bank both participate in a data-sharing federation. Each bank runs subnetworks of its own:
```mermaid theme={null}
graph TD
Fed[Federation Root]
Coastal[Coastal Bank]
Summit[Summit Bank]
CCards[Coastal — Credit Cards]
SAuto[Summit — Auto Loans]
Fed --> Coastal
Fed --> Summit
Coastal --> CCards
Summit --> SAuto
```
**Initially**, with no opt-in rules anywhere:
* Coastal can query Coastal and *Coastal — Credit Cards* (descendant). It **cannot** query Summit or *Summit — Auto Loans*: Summit is a cousin, and Summit's subnetwork is also a cousin (common ancestor: the Federation).
* The Federation root's querier (if the federation operator holds one) can query every network in the tree — everything is its descendant.
**The federation decides cousins should be able to screen against each other's data.** The fix is one switch in one place: the Federation governor enables `cousin` **on the Federation root** — the closest common ancestor of Coastal and Summit. Now a Coastal querier can scope queries to Summit (and vice versa), and likewise across the cousin subnetworks.
**Common mistakes in this topology:**
* **Setting `cousin` on Coastal or Summit does nothing.** The rule is consulted on the closest common ancestor, not on either leaf. Neither bank can open (or, alone, close) the cousin path — that authority sits with the Federation governor, which is the point: cross-bank sharing is a federation-level agreement.
* **Expecting `cousin` on a network *above* the Federation to work.** If the Federation itself had a parent, enabling `cousin` there would not authorize Coastal ↔ Summit; only their *closest* shared ancestor's setting is consulted.
* **Expecting Coastal to see Summit's subnetworks in one query.** Authorizing Coastal → Summit doesn't bundle in *Summit — Auto Loans*. For that, Summit's governor would additionally enable `auto_include_descendants` on Summit's network, widening any authorized Summit-scoped query to include its subnetworks.
And as always: authorization gets Coastal's query in the door at Summit; what rows come back is governed by Coastal's [entitlement](/concepts/governance/entitlement), and which fields appear is governed by the applicable [querying policy](/concepts/governance/querying-policies). A bank that has never furnished or queried a given consumer gets an empty (but successful) result, not a treasure trove.
## Who can change governance rules
Only the **governor entity** of a network can change that network's governance rules. A governor cannot change rules on a parent, child, or cousin network without separately being its governor.
The governor entity is the single entity recorded as the owner of the network when it was created. See the footnote on [Network Roles](/concepts/governance/network-roles) for the distinction between the governor entity and entities holding the governor role.
There is no field-level permissioning *inside* the governance settings today — a governor who can edit the network at all can change any of the three rules.
## Rules can only be enabled from above
Because each rule is stored on the network being opened up — not on the network that wants access — only the governor of the *opening* network can turn it on. This has an important consequence:
> A child network's governor cannot grant themselves visibility into the parent by enabling `ancestor_query`. That switch lives on the *parent*, and only the parent's governor can flip it.
The same property holds for `cousin`: the rule sits on the closest common ancestor of the two cousin networks, so opening up cross-cousin visibility requires a governor at that ancestor — not a governor at either of the two leaf networks.
This is a deliberate property of the model. Access flows from the top down, configured by the senior network's governor; a junior network cannot elevate itself into a senior network's data.
If the *same* entity happens to be the governor of both networks (for example, a bank that governs both itself and a subnetwork underneath it), that entity can edit both networks' settings — but that is the same entity already holding authority over both networks, not a child governor reaching up.
## Current limitations
The governance rule format is provisional and **subject to change**. The three keys above (`ancestor_query`, `cousin`, `auto_include_descendants`) represent the rules SOLO supports today. We expect the surface area to grow — for example, with per-entity overrides, role-specific rules, or expiry windows — and the key names may change as the schema matures. This page will be updated when the schema is finalized.
Other things to be aware of in the current model:
* **Rules are network-wide.** A rule applies uniformly to every querier it could affect. There is no way today to enable `cousin` for some cousin networks but not others.
* **No validation on unknown keys.** Anything else written into the governance settings is silently ignored. Be careful about typos — `ancestor_querys` (with an s) will read as "off" rather than producing an error.
If you need behaviour that the current rules don't express, contact your SOLO account manager — we are actively gathering input on the next iteration of the governance model.
## What's next
* See [Networks](/concepts/governance/networks) for the underlying tree structure.
* See [Network Roles](/concepts/governance/network-roles) for who can participate in a network and how.
# Network Roles
Source: https://docs.solo.one/concepts/governance/network-roles
The three ways an entity can participate in a network
Every entity that participates in a [network](/concepts/governance/networks) does
so in one or more **roles**. The role determines what the entity is allowed to do
on that network — administer it, supply data to it, or query data from it.
## The three roles
Administers the network. Governors manage settings, add and remove participants, and configure how data is allowed to flow between this network and others.
Supplies data into the network. Furnishers write records that other participants can later query, subject to the network's governance rules.
Reads data from the network. Queriers retrieve records that have been furnished by other participants in the network.
## What each role is responsible for
### Governor
The governor is the network's administrator and the accountable party for its
configuration. In practice a governor:
* **Manages the participant roster** — adds entities to the network with the
roles they need, and removes them when an arrangement ends.
* **Owns the policy surface** — creates and configures the
[querying policies](/concepts/governance/querying-policies) and
[furnishing policies](/concepts/governance/furnishing-policies) that govern
each product in the network.
* **Sets governance rules** — decides whether related networks may query this
one (see [Network Governance](/concepts/governance/network-governance)).
* **Introduces subnetworks** — subnetwork definitions furnished through bulk
ingestion are accepted only when the caller governs the target network.
A governor seat is **not** a window into other members' data. Reading data
still requires the querier role plus [consent](/concepts/identity/consent) and
[entitlement](/concepts/governance/entitlement), like any other participant.
### Furnisher
The furnisher contributes data. A furnisher:
* **Furnishes product records** — calls `POST /v1/products/{product}/furnish`
(or bulk paths like [SFTP](/api-overview/sftp/overview) and
[file upload](/api-overview/furnishing/file-upload)) naming the network,
subnetwork, and application date.
* **Earns entitlement by contributing** — each entity a furnisher contributes
data about becomes an entity whose consolidated data the furnisher may read
back on future queries. See [Entitlement](/concepts/governance/entitlement).
A furnisher does not need to know which furnishing policy applies — the network
resolves it automatically from the subnetwork and application date.
### Querier
The querier consumes data. A querier:
* **Creates consent records** — querying a consumer or business requires a
valid [consent](/concepts/identity/consent), so the consent endpoints are part
of the querier's day-to-day surface.
* **Runs product queries** — calls `POST /v1/products/{product}/query` scoped
to the networks it participates in (or networks reachable through
[governance rules](/concepts/governance/network-governance)).
* **Searches entities** — looks up consumers and businesses within the
network before querying.
What a query returns is never determined by the role alone: the
[querying policy](/concepts/governance/querying-policies) caps what the product
may expose in that network, and [entitlement](/concepts/governance/entitlement)
narrows it to what the caller has earned.
## Role-to-endpoint capability matrix
The table below maps the public `/v1` API surface to the role that exercises
it. ✓ marks the role an operation belongs to; — means the operation is not
part of that role's job.
| Endpoint | Governor | Furnisher | Querier |
| ---------------------------------------------------------------- | :------: | :-------: | :-----: |
| `POST /v1/consent/consumer` | — | — | ✓ |
| `GET /v1/consent/consumer/{consent_id}` | — | — | ✓ |
| `PUT /v1/consent/consumer/{consent_id}` | — | — | ✓ |
| `POST /v1/consent/business` | — | — | ✓ |
| `GET /v1/entities/consumer/search` | ✓ | ✓ | ✓ |
| `GET /v1/entities/business/search` | ✓ | ✓ | ✓ |
| `POST /v1/products/{product}/query` | — | — | ✓ |
| `POST /v1/products/check` (coverage check) | — | — | ✓ |
| `POST /v1/products/{product}/furnish` | — | ✓ | — |
| `POST /v1/file-upload/ingest` (data workbooks) | — | ✓ | — |
| `POST /v1/file-upload/ingest` (subnetwork / policy workbooks) | ✓ | — | — |
| `POST /v1/networks/policies/querying` | ✓ | — | — |
| `PUT /v1/networks/policies/querying/{policy_id}/configuration` | ✓ | — | — |
| `POST /v1/networks/policies/furnishing` | ✓ | — | — |
| `PUT /v1/networks/policies/furnishing/{policy_id}/configuration` | ✓ | — | — |
| `GET /v1/networks/policies/furnishing` | ✓ | ✓ | ✓ |
| `GET /v1/networks/policies/furnishing/{policy_id}` | ✓ | ✓ | ✓ |
| `POST /v1/institution-attestation` | ✓ | ✓ | ✓ |
**How to read this matrix.** It describes the *operational model* — which role
each operation belongs to in a well-run network. Enforcement happens at
different layers depending on the operation. Network administration (managing
memberships, accepting subnetwork and policy definitions) is hard-enforced
against the network's governor. Reads are enforced through network scoping,
[consent](/concepts/identity/consent), and
[entitlement](/concepts/governance/entitlement) — so calling a query endpoint
without the underlying permissions yields an authorization error or an empty
result rather than data. Don't treat a missing per-endpoint role check as a
grant of access; SOLO may tighten per-endpoint enforcement over time.
Rows marked for all three roles are member-level operations: any participant
can search the directory of entities within its network scope, read the
furnishing policies visible in a network it belongs to, or attest for its own
institution.
## What each role gates
Each role gates a specific kind of action on the network, and the three gates
are independent of one another:
| Role | Gates |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Governor** | Editing the network's settings, adding and removing participants, configuring governance rules, authoring policies, introducing subnetworks |
| **Furnisher** | Writing data into the network |
| **Querier** | Reading data from the network |
The gates do not overlap. An entity holding only the furnisher role can write
data but cannot read it back out — reading requires the querier role on the
same network (or, via
[Network Governance](/concepts/governance/network-governance), on a related
one). The same is true in reverse: a querier cannot furnish data.
This is why most operators who both supply and consume data on a network hold
both the furnisher and querier roles.
## Roles are additive
An entity can hold more than one role on the same network. A bank that operates
a network it also furnishes into and queries from will typically hold all three
roles. SOLO records each role as a separate participation, so revoking one role
does not affect the others.
The same entity cannot be granted the same role twice on the same network —
each combination of network, entity, and role is unique.
## Roles are network-specific
Roles do not transfer between networks. An entity that is a governor of one
network has no automatic role on its parent, child, or cousin networks. To
participate elsewhere, the entity must be added explicitly to each network it
operates on.
What *does* transfer is **query access** — a querier on one network may,
depending on governance rules, be allowed to query data scoped to a related
network. See [Network Governance](/concepts/governance/network-governance) for
the full rules.
## Who can change roles
Only a governor of a network can add or remove participants on that network. A
governor of a parent network cannot directly add or remove roles on a child
network — they would need to be a governor of the child as well.
Two safety rules apply, and both are enforced server-side:
* **A network must always have at least one governor.** SOLO rejects any
change — demotion, reassignment, or removal — that would leave a network with
zero governors.
* **An entity can always remove its own participation** in a network (as long
as doing so wouldn't violate the rule above).
Attempts that violate these rules fail with an authorization error in the
standard envelope (see [Errors](/home/errors)):
```json theme={null}
{
"detail": "Permission denied",
"error_code": "PERMISSION_DENIED"
}
```
## What's next
* See [Network Governance](/concepts/governance/network-governance) for how
participation translates into query access across the network tree.
* See [Entitlement](/concepts/governance/entitlement) for why holding a role is
necessary but not sufficient to read data.
## In the dashboard
***
**A note on governors.** SOLO currently distinguishes between a network's *governor entity* (a single entity recorded on the network itself, set when the network is created) and entities holding the *governor role* (which can be more than one). In practice these are kept in sync — the entity that creates a network is recorded as the governor entity and is also granted the governor role automatically. Today, only the entity recorded as the governor entity can edit the network's settings, even if other entities hold the governor role. This distinction is under review and may be removed in a future release; we will update this page when the decision is final.
# Networks
Source: https://docs.solo.one/concepts/governance/networks
The trust boundary that groups participants, roles, products, and policies
A **network** is the trust boundary of p SOLO platform. It groups the
participants who have agreed to share data, the **products** they can use, the
**policies** that govern those products, and the data that gets furnished.
Everything you do — furnishing, querying, applying policies — happens *within* a
network you belong to.
## What a network represents
A network is a logical grouping that ties together three things:
* **Participants** — the entities that operate within the network (see
[Network Roles](/concepts/governance/network-roles))
* **Data** — every furnished record carries a network reference, so the network
acts as a boundary on what data can be read
* **Rules** — the [querying policies](/concepts/governance/querying-policies) and
[furnishing policies](/concepts/governance/furnishing-policies) that govern each
product, and how data is allowed to move between this network and neighbouring
networks (see [Network Governance](/concepts/governance/network-governance))
A network has a name, an optional description, and a single **governor entity**
that owns it.
## The network tree
Networks are not flat. Every network has at most one **parent network**, so
networks form a **tree** where smaller, more specific networks live underneath
broader ones. A network with no parent sits at the top of the tree and is called
a **root** network. Networks below it are its **descendants**; the networks
directly under it are its **children**.
```mermaid theme={null}
graph TD
Federation[Federation Root]
BankA[Coastal Bank]
BankB[Summit Bank]
BankASub1[Coastal — Credit Cards]
BankASub2[Coastal — Mortgages]
BankBSub1[Summit — Auto Loans]
Federation --> BankA
Federation --> BankB
BankA --> BankASub1
BankA --> BankASub2
BankB --> BankBSub1
```
In the diagram above, *Coastal Bank* is a child of *Federation Root* and a parent
of *Coastal — Credit Cards*. *Summit Bank* is a **sibling** of *Coastal Bank*
because they share a parent. *Coastal — Credit Cards* and *Summit — Auto Loans*
are **cousins** because they share a common ancestor (the Federation) without
being in each other's direct line.
This lets a single arrangement — such as a sponsor bank and the fintechs it
sponsors — share a parent network while keeping each participant's data within
its own boundary.
## Relations between networks
These are the words SOLO uses to describe how any two networks relate. They show
up throughout the permissioning model, so it's worth getting them straight up
front.
| Term | Meaning |
| -------------- | -------------------------------------------------------------------------------------------------- |
| **Parent** | The network directly above this one. |
| **Child** | A network whose parent is this one. |
| **Ancestor** | Any network reachable by walking upward — parent, grandparent, and so on, up to the root. |
| **Descendant** | Any network reachable by walking downward — child, grandchild, and so on. |
| **Sibling** | Two networks that share the same parent. |
| **Cousin** | Two networks that share a common ancestor but are not in each other's ancestor or descendant line. |
| **Root** | A network with no parent. |
SOLO treats siblings as a special case of cousins for permissioning purposes.
Anywhere governance rules talk about cousins, siblings are included.
## Standard networks and subnetworks
Networks come in two kinds, distinguished by their `network_type`:
* **Standard networks** are the default. A bank, a consortium, or a fintech
operating in SOLO each runs as a standard network. These are typically created
through the SOLO Dashboard.
* **Subnetworks** are child networks of a standard network that represent a
specific product or initiative the operator runs — a credit card line, a
mortgage line, a partnership offering. Subnetworks are usually created
automatically by SOLO's data-furnishing workflows when a subnetwork definition
first arrives through bulk ingestion (see [Network lifecycle](#network-lifecycle)).
Subnetworks always sit beneath a standard parent network. They behave like any
other network in the tree — they can have their own participants, their own
data, and their own governance settings — but their lifecycle is typically tied
to the upstream data flow that created them.
When you furnish, you name the **subnetwork** so the network can route the data
and resolve the right [furnishing policy](/concepts/governance/furnishing-policies):
```json theme={null}
{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "acme-lending",
"application_date": "2026-05-28",
"records": [ /* … */ ]
}
```
If you operate a single product line, you probably only need a standard network.
Subnetworks become useful when you want to keep data, participants, or rules
isolated between distinct product offerings under the same operator.
## Network lifecycle
Standard networks and subnetworks come into existence in different ways, and
it's worth understanding both paths.
### Standard networks: created through the dashboard
Standard networks are created deliberately, through the **SOLO Dashboard**, as
part of onboarding your organization or standing up a new arrangement (a
consortium, a sponsorship structure, a new line of business). A new network
needs:
* a **name** and an optional **description**,
* an optional **parent network**, which places it in the tree, and
* a **governor entity** — the entity that owns and administers it.
The entity that creates the network is recorded as its governor entity, and
SOLO automatically provisions that entity's **governor membership** on the new
network at the same time. This means a freshly created network is never
ownerless: from the first moment, exactly one entity can administer it, add
participants, and configure its [governance
rules](/concepts/governance/network-governance).
After creation, the governor typically:
1. adds participants with the [roles](/concepts/governance/network-roles) they need
(furnisher, querier, or additional governors),
2. creates and configures the [querying
policies](/concepts/governance/querying-policies) and [furnishing
policies](/concepts/governance/furnishing-policies) for the products the network
will use, and
3. optionally enables governance rules to open query paths to related networks.
### Subnetworks: created by furnishing workflows
Subnetworks usually aren't created by hand. They are created
**automatically by SOLO's data-furnishing workflows** when a subnetwork
definition first arrives through bulk ingestion — typically a subnetworks
workbook delivered over [SFTP](/api-overview/sftp/overview) or via [file
upload](/api-overview/furnishing/file-upload). The workflow creates a child network
with `network_type: subnetwork` underneath the standard network the definition
was furnished into, named after the subnetwork, and links it to the [furnishing
policies](/concepts/governance/furnishing-policies) named in the definition.
Two properties of this flow are worth knowing:
* **Only the network's governor can introduce subnetworks.** Subnetwork
definitions furnished by an entity that doesn't govern the target network are
rejected.
* **Subnetwork names are unique within their parent network.** A definition that
re-uses an existing subnetwork name in the same network fails for that row
rather than silently overwriting the existing subnetwork.
From then on, every furnish that names that `subnetwork_name` is routed into the
subnetwork, and the subnetwork's furnishing-policy assignments determine how
the data is accepted (see [Furnishing
Policies](/concepts/governance/furnishing-policies)). Furnishing data with a
`subnetwork_name` that doesn't exist in the target network is an error — the
subnetwork definition has to arrive first.
Because a subnetwork's lifecycle is tied to the upstream data flow that created
it, you generally don't delete or restructure subnetworks directly — you stop
furnishing into them, or deprecate the policies assigned to them.
## Permissions & roles
Your organization joins a network with one or more **roles**. Roles describe what
you can do, not what data you can see.
| Role | What it allows |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Governor** | Administer the network — configure policies and directory metadata. A governor seat is **not** a backdoor to other members' data. |
| **Furnisher** | Contribute data into the network for a product. |
| **Querier** | Run product queries scoped to the network. |
A single organization can hold several roles at once (for example, both furnisher
and querier). See [Network Roles](/concepts/governance/network-roles) for the full model.
Governing a network does **not** automatically let you read what other members
have furnished. Reading another participant's data still requires
[consent](/concepts/identity/consent) and [entitlement](/concepts/governance/entitlement). Roles
grant *capabilities*, not *visibility*.
## Policy usage within a network
A network ties products to participants through policies:
* A **[querying policy](/concepts/governance/querying-policies)** binds a product
to the network and defines, field by field, what queriers may read.
* A **[furnishing policy](/concepts/governance/furnishing-policies)** defines how
furnished data for a product is accepted, validated, and routed.
When you query, you scope the request to one or more network ids and a querying
policy, so the network knows both *where* to read and *which* reading rules
apply. When you furnish, the network resolves the applicable furnishing policy
from the network, subnetwork, and application date automatically — you never
pass a furnishing policy id yourself.
```mermaid theme={null}
flowchart LR
M[Your organization] -->|querier| QP[Querying policy]
M -->|furnisher| FP[Furnishing policy]
QP --> Prod[Product]
FP --> Prod
Prod --> Net[(Network data)]
```
## Joining a network
Membership and roles are arranged with your SOLO account manager. Once you're a
member, you can:
* **search** for entities within the network,
* **furnish** products into it (as a furnisher), and
* **query** products from it (as a querier, with [consent](/concepts/identity/consent)).
See the [quickstart](/home/quickstart/join-a-network) for a walkthrough.
## In the dashboard
## Related concepts
How entities participate in a network.
How data can flow between related networks.
The field-level read rules a network applies to each product.
How furnished data is accepted, validated, and routed.
Why network membership alone doesn't grant data access.
# Querying Policies
Source: https://docs.solo.one/concepts/governance/querying-policies
Field-by-field read rules that bind a product to a network
A **querying policy** is the set of read rules a
[network](/concepts/governance/networks) applies to a
[product](/api-overview/querying/products). It binds the product to the network and
defines, field by field, what queriers may read — and from which sources.
The same product can be configured differently in different networks. Querying
policies are how a network tailors a shared product schema to its own
agreements: expose these fields, suppress those, prefer this furnisher's data
over that one's.
```mermaid theme={null}
flowchart LR
Prod[Product] --- NP[Network-product association]
Net[Network] --- NP
NP --- Pol[Querying policy]
Pol --> Models[Per-model selections]
Models --> Fields[Per-field rules:
enabled, choice, provenance order]
```
Querying policies govern *reads*. The write-side counterpart — how furnished
data is accepted, validated, and routed — is the
[furnishing policy](/concepts/governance/furnishing-policies).
## Why querying policies exist
Products define a standard schema — every KYC certificate has the same shape
everywhere. But networks have different agreements about what may be shared: a
closed consortium might expose document-verification outcomes that a broader
federation suppresses; one network might accept any furnisher's biometric data
while another only trusts a named source. Querying policies let a network:
* expose only the fields appropriate for its participants,
* rank which **sources** (provenances) a field may be answered from, in order
of preference,
* keep read rules explicit, versionable, and auditable, and
* evolve those rules without changing the underlying product or any
integration code — queriers keep calling the same endpoint.
Because the policy hangs off the product-network association rather than the
product itself, the same product can run under strict rules in one network and
permissive rules in a sibling network, simultaneously.
## What a policy contains
A querying policy hangs off the **network-product association** (the pairing of
one product with one network) and carries:
| Attribute | Meaning |
| ------------ | ------------------------------------------------------------------------- |
| `name` | Display name, unique per product-network pairing for its author. |
| `is_default` | Whether this is the policy used when a query doesn't name one explicitly. |
| `enforced` | Whether the policy's selections are applied to reads. |
| `status` | Lifecycle state: `draft`, `published`, or `archived`. |
Beneath the policy sit **per-model selections** — one for each product model
the policy has an opinion about — and beneath those, **per-field rules**:
* **`is_enabled`** — whether the field may be read at all under this policy.
* **`boolean_choice`** — for fields with a constrained choice, which value the
policy requires.
* **`selected_provenance_option_ids`** — an *ordered* list of acceptable data
sources for the field. Order matters: the first option is tried first, and
later options are lower priority.
A model with no selections is explicitly excluded from provenance
customization; a model the policy doesn't mention at all falls back to the
product's defaults.
## Lifecycle
Policies carry a `status` of `draft`, `published`, or `archived`. New revisions
are authored as drafts (the dashboard's version wizard works this way); saving
a configuration through the API leaves the policy in `published` status, at
which point it governs live queries. Archived policies are retained for audit
but no longer selectable.
Saving a configuration **replaces** the policy's per-model and per-field
selections with the submitted set — it is not a patch. Always submit the
complete intended configuration.
## Creating a policy
Creating a querying policy is a two-step flow: create the policy shell, then
configure its field selections.
### Step 1 — create
`POST /v1/networks/policies/querying` with the product, the network, and a
name. The server resolves the product-network association — creating it if it
doesn't exist yet — and returns the new policy.
```bash theme={null}
curl -X POST https://api.solo.one/v1/networks/policies/querying \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "1b9a4c33-…",
"network_id": "9f1c0c2e-…",
"name": "Coastal KYC — standard read"
}'
```
```json theme={null}
{
"id": "5e7d2a14-…",
"network_product_id": "c4f8e6d1-…",
"entity_id": "7c2d91e4-…",
"name": "Coastal KYC — standard read",
"is_default": false
}
```
### Step 2 — configure fields
`PUT /v1/networks/policies/querying/{policy_id}/configuration` with the full
set of per-model, per-field selections:
```bash theme={null}
curl -X PUT https://api.solo.one/v1/networks/policies/querying/5e7d2a14-…/configuration \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model_selections": [
{
"product_model_definition_id": "0a1b2c3d-…",
"field_selections": [
{
"product_field_definition_id": "e5f6a7b8-…",
"is_enabled": true,
"boolean_choice": null,
"selected_provenance_option_ids": ["11aa22bb-…", "33cc44dd-…"]
},
{
"product_field_definition_id": "f9e8d7c6-…",
"is_enabled": false,
"boolean_choice": null,
"selected_provenance_option_ids": []
}
]
}
]
}'
```
```json theme={null}
{
"id": "5e7d2a14-…",
"name": "Coastal KYC — standard read",
"status": "published"
}
```
You can optionally include a `"name"` in the configuration body to rename the
policy in the same call.
Policy authoring is a [governor](/concepts/governance/network-roles)
responsibility. Queriers consume policies; they don't write them.
## How a query selects a policy
When you [query a product](/api-overview/querying/overview), the request body
carries the network scope and, optionally, the policy:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"],
"policy_id": "5e7d2a14-…"
}'
```
* **`network_ids`** (required) — one or more networks to read across.
* **`policy_id`** (optional) — the querying policy to apply. The single policy
is applied across every network in `network_ids`. **If omitted, each
network's default policy** (`is_default: true`) **is used.**
The response echoes the resolution per network, so you always know which
rules shaped each result:
```json theme={null}
{
"results": [
{ "meta": { "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" }, "…": "…" }
]
}
```
### Resolution and audit stamping
Every query event is stamped with the querying policy that governed it, so
results are auditable after the fact. Resolution works through a fixed
fallback chain:
1. the policy id **explicitly stamped** on the query event;
2. failing that, a policy id found in the **request or response payload**;
3. failing that, the policy referenced by **certificates linked** to the query;
4. failing that, the **network's default policy** — matched first against the
querying entity's own policies, then against the network governor's.
As a caller you only ever interact with step 1 (pass `policy_id`) or step 4
(omit it and rely on the default). Steps 2–3 are how SOLO back-fills the audit
trail for historical events.
If your network has exactly one published policy per product and it's marked
default, queriers can omit `policy_id` entirely and always get the right
rules. Reserve explicit `policy_id` for networks that run multiple read
profiles side by side.
### Querying across multiple networks
`network_ids` accepts more than one network, and the listed `policy_id` (or
each network's default) is applied across all of them. Two things to keep in
mind when fanning a query out:
* **Authorization is per network.** Each network in the list is checked
independently against your memberships and the
[governance rules](/concepts/governance/network-governance). A network you
can't scope to fails the query with an authorization error rather than being
silently dropped.
* **The policy still only caps; it doesn't fetch.** Adding networks widens
where SOLO looks, but [entitlement](/concepts/governance/entitlement) still
filters every record to what you've earned, regardless of how many networks
the query touches.
## The policy is a ceiling, not a grant
A querying policy defines what the product **may** expose in the network — for
anyone. What *you* get back is further narrowed by
[entitlement](/concepts/governance/entitlement):
| Question | Answered by |
| -------------------------------------------------------------- | ------------------- |
| *What may be read from this network, at most?* | **Querying policy** |
| *What may* ***I*** *read, given my history with this subject?* | **Entitlement** |
A query returns the **intersection**: fields the policy permits **and** records
you are entitled to. Enabling a field in the policy does not push data to
anyone who hasn't earned it; disabling a field hides it even from participants
who furnished it through this product in this network.
## Errors you may encounter
All errors use the standard envelope (see [Errors](/home/errors)):
```json theme={null}
{ "detail": "…", "error_code": "…" }
```
| Situation | Status / code |
| --------------------------------------------------------------------- | ------------------------------------ |
| Configuration references an unknown `policy_id` | `404` `RESOURCE_NOT_FOUND` |
| Creating a duplicate policy (same product-network pairing, same name) | `409` `RESOURCE_CONFLICT` |
| Malformed selection payload (missing field, bad UUID) | `422` (short shape, no `error_code`) |
| Querying a network you can't scope to | `403` `PERMISSION_DENIED` |
## In the dashboard
## Related concepts
The write-side counterpart: how data gets into the network.
The per-participant layer that narrows what a policy exposes.
How policies plug into a product query.
The boundary policies are scoped to.
# Consent
Source: https://docs.solo.one/concepts/identity/consent
Recording permission to query a consumer or business
**Consent** is your record that you have permission to query a specific consumer
or business. Querying personal and business data carries a legal obligation to
have a permissible purpose — consent is how the network captures, and can later
prove, that you had one.
A consent record is two things at once:
* **A key.** You create it once per subject per network, receive a
`consent_id`, and pass that `consent_id` on every product query for the
subject. Without a valid `consent_id`, consumer and business queries are
rejected.
* **An audit artifact.** The record permanently captures *who* consented, *as
identified by which details*, *when*, and *with what scope* — so that every
query you ever ran can be traced back to a lawful basis.
That dual nature drives the API's design, including its strictest rule: the
identity on a consent record can never be edited.
## Lifecycle
A consumer consent record moves through a simple lifecycle, with one endpoint
for each transition you control:
```mermaid theme={null}
flowchart LR
A[Gather permission
from the subject] --> B["Create
POST /v1/consent/consumer"]
B --> C[consent_id]
C --> D["Query products
POST /v1/products/…/query"]
C --> E["Read back
GET /v1/consent/consumer/{consent_id}"]
C --> F["Update scope / expiry / fields
PUT /v1/consent/consumer/{consent_id}"]
F --> D
F --> G[Expired
per expires_at]
```
You gather the subject's permission **outside** the API — in your onboarding
flow, your terms, your call script — then record it. From there the
`consent_id` is reusable on queries until the consent expires, and you can read
it back or adjust its mutable attributes at any time.
## What a consent record contains
All three consumer endpoints return the same shape:
| Field | Type | Notes |
| --------------------- | -------------- | ------------------------------------------------------------------- |
| `consent_id` | string | The token you pass on product queries |
| `field_access_grants` | array | Grants attached to this consent — see [below](#field-access-grants) |
| `created_at` | string | When the record was created |
| `scope` | string | Free-form scope label; empty until you set one |
| `expires_at` | string \| null | When the consent lapses; `null` means no expiry recorded |
| `consented_fields` | string \| null | Free-form description of the fields the subject consented to |
| `events` | string | Event history for the record |
## Creating consent
`POST /v1/consent/consumer` records consent server-to-server. You supply the
subject's identity and attest that you gathered their permission beforehand.
| Body field | Required | Notes |
| ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `network_id` | Yes | The [network](/concepts/governance/networks) this consent belongs to |
| `did_gather_consent_from_consumer_prior` | Yes | Must be `true` — see warning below |
| `consumer_consent_identity` | Yes | `first_name`, `last_name`, `date_of_birth`, `personal_email`, `phone_number`, `social_security_number` |
| `consumer_id` | No | Link to an existing consumer profile found via [entity search](/concepts/identity/entities) |
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/consumer \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"did_gather_consent_from_consumer_prior": true,
"consumer_consent_identity": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"personal_email": "jane@example.com",
"phone_number": "+14155550100",
"social_security_number": "123-45-6789"
}
}'
```
A `200 OK` returns the full record, including a default field access grant
created alongside the consent:
```json theme={null}
{
"consent_id": "a3f0b9c7-…",
"field_access_grants": [
{
"furnishing_entity_id": null,
"field_definitions": [
"fraud_verification_event.confirmed_fraud_indicator",
"fraud_verification_event.fraud_attribute_label",
"fraud_verification_event.fraud_event_date"
],
"effective_from": "2026-06-09",
"effective_to": null
}
],
"created_at": "2026-06-09T22:14:03.512000+00:00",
"events": "",
"scope": "",
"expires_at": null,
"consented_fields": null
}
```
`did_gather_consent_from_consumer_prior: true` is an **attestation** that you
obtained the consumer's permission before making this call. Sending `false`
is rejected with `400 VALIDATION_ERROR` — the network will not create a
consent record that asserts no consent was gathered.
### Linking identity: `consumer_id` vs. the identity payload
How the consent attaches to an [entity](/concepts/identity/entities) depends on whether
you pass `consumer_id`:
* **With `consumer_id`** — the consent links to that existing consumer profile.
Use this after a successful
`GET /v1/entities/consumer/search`, so the subject's history stays
consolidated on one profile.
* **Without `consumer_id`** — a new consumer profile is created from the
identity payload and linked to the consent.
Either way, the identity details you submitted are stamped onto the consent
record itself as the audit trail of who consented.
## Using a consent ID
Pass the `consent_id` in the body of a product query. The network resolves the
consent to the subject's identity and applies the appropriate field access — you
don't repeat the subject's identifying details on every call.
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_policy": [
{ "network_id": "9f1c0c2e-…", "policy_id": "5e7d2a14-…" }
]
}'
```
See [Querying your first product](/home/quickstart/query-a-product) for the
end-to-end walkthrough.
## Reading consent back
`GET /v1/consent/consumer/{consent_id}` returns the current state of a consent
record. The `network_id` query parameter is **required** — the lookup is scoped
to that network:
```bash theme={null}
curl -G https://api.solo.one/v1/consent/consumer/a3f0b9c7-… \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…"
```
The response is the same shape as create. If the `consent_id` doesn't exist
**or** exists in a different network, you get a `404`:
```json theme={null}
{
"detail": "Consent record not found",
"error_code": "RESOURCE_NOT_FOUND"
}
```
A consent record belongs to one network. The same subject needs a separate
consent in each network you query them through, and a `consent_id` minted in
one network is invisible — `404`, not `403` — from another.
## Updating consent
`PUT /v1/consent/consumer/{consent_id}` changes the **mutable attributes** of a
consent record: its scope, its expiry, and the description of consented fields.
| Body field | Required | Notes |
| ------------------ | --------------------------- | --------------------------------- |
| `network_id` | Yes | Network scope — same rule as read |
| `scope` | At least one of these three | New scope label |
| `expires_at` | | New expiry timestamp (ISO 8601) |
| `consented_fields` | | New consented-fields description |
```bash theme={null}
curl -X PUT https://api.solo.one/v1/consent/consumer/a3f0b9c7-… \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"scope": "kyc",
"expires_at": "2027-06-09T00:00:00Z"
}'
```
The response is the **post-update** record:
```json theme={null}
{
"consent_id": "a3f0b9c7-…",
"field_access_grants": [
{
"furnishing_entity_id": null,
"field_definitions": [
"fraud_verification_event.confirmed_fraud_indicator",
"fraud_verification_event.fraud_attribute_label",
"fraud_verification_event.fraud_event_date"
],
"effective_from": "2026-06-09",
"effective_to": null
}
],
"created_at": "2026-06-09T22:14:03.512000+00:00",
"events": "",
"scope": "kyc",
"expires_at": "2027-06-09T00:00:00+00:00",
"consented_fields": null
}
```
Omitting all three updatable fields is a `400`:
```json theme={null}
{
"detail": "At least one of scope, expires_at, or consented_fields must be provided",
"error_code": "VALIDATION_ERROR"
}
```
### Identity is immutable — by design
The update endpoint deliberately accepts **only** `scope`, `expires_at`, and
`consented_fields`. The subject's name, date of birth, email, phone, and SSN on
a consent record can never change.
A consent record is an audit artifact of **who consented and when**. If the
identity could be edited after the fact, the record could no longer prove
which person's permission authorized your past queries. To consent with
different identity data — a legal name change, a corrected SSN — create a
**new** consent record and use its `consent_id` going forward.
## Field access grants
Each consent record carries one or more **field access grants** in
`field_access_grants`. A grant is a concrete statement of *which fields, from
which furnisher, over which time window* this consent unlocks.
| Grant field | Type | Meaning |
| ---------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `furnishing_entity_id` | string \| null | The furnisher whose contributed data this grant covers. `null` means the grant is not pinned to a specific furnisher. |
| `field_definitions` | array of strings | The fields covered, as `source_table.source_column` strings — e.g. `fraud_verification_event.fraud_event_date`. |
| `effective_from` | string \| null | Date the grant becomes active. |
| `effective_to` | string \| null | Date the grant lapses; `null` means open-ended. |
When you create a consent, a default grant is attached automatically with
`effective_from` set to the creation date and a baseline set of field
definitions. Grants outside their effective window no longer apply.
Grants describe what this **consent** covers. They sit alongside — not in
place of — [entitlement](/concepts/governance/entitlement): a field appears in a query
result only if the consent covers asking, the network's
[querying policy](/concepts/governance/querying-policies) exposes the field, and you have
earned entitlement to it.
## Failure modes
| Status | `error_code` | When | Example `detail` |
| ------ | -------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `400` | `VALIDATION_ERROR` | `did_gather_consent_from_consumer_prior` is `false` on create | `Consent cannot be created without prior consumer consent gathering` |
| `400` | `VALIDATION_ERROR` | Update with none of `scope`, `expires_at`, `consented_fields` | `At least one of scope, expires_at, or consented_fields must be provided` |
| `404` | `RESOURCE_NOT_FOUND` | Unknown `consent_id`, or a consent from a different network | `Consent record not found` |
| `422` | — | Request shape errors: missing required fields, wrong types | `body -> consumer_consent_identity -> date_of_birth: value is not a valid datetime` |
Error responses follow the standard envelope — see
[Errors](/home/errors):
```json theme={null}
{
"detail": "Consent cannot be created without prior consumer consent gathering",
"error_code": "VALIDATION_ERROR"
}
```
## Business consent
`POST /v1/consent/business` records consent to query a business, mirroring the
consumer flow:
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/business \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"did_gather_consent_from_business_prior": true,
"business_consent_identity": {
"business_legal_name": "Acme Corp",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier_from_jurisdiction_of_formation": 1234567,
"business_tax_identifier_type": "EIN",
"business_tax_identifier_value": "12-3456789"
},
"identity": {
"business_legal_name": "Acme Corp",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier_from_jurisdiction_of_formation": 1234567
}
}'
```
The response contains `consent_id`, `consented_fields`, `created_at`, `events`,
and `scope`. Use the `consent_id` on business product queries exactly as you
would a consumer one.
Business consent currently supports **create only**. There is no
`GET` or `PUT /v1/consent/business/{consent_id}` today — read-back and
updates are consumer-only. If you need to change a business consent's
attributes, create a new record and switch to its `consent_id`.
## How consent relates to entitlement
Consent and [entitlement](/concepts/governance/entitlement) are complementary gates, and a
query needs both:
| | Consent | Entitlement |
| -------------------- | ---------------------------------------- | --------------------------------------------- |
| Question it answers | *May I query this subject at all?* | *Which fields come back?* |
| How you get it | Record permission, get a `consent_id` | Earn it by furnishing or querying the subject |
| Scope | One subject, in one network | Per field, per entity |
| Carried on the query | `consent_id` in the request body | Applied automatically by the network |
| Can it change? | Mutable scope/expiry; immutable identity | Accrues with each furnish and query |
Consent without entitlement yields a permitted query with little or no data;
entitlement without consent yields no query at all.
## Common questions
Yes. Create one consent per subject per network and reuse its `consent_id`
on every product query for that subject while the consent is valid. You do
not need a new consent record for each query.
The read is scoped by the `network_id` query parameter. The most common
cause is passing a different `network_id` than the one the consent was
created under — the record exists, but it isn't visible from that network,
so the API reports `404 RESOURCE_NOT_FOUND` rather than confirming it
exists elsewhere.
**Update** when the subject's permission itself changed shape — a broader
or narrower scope, a new expiry date, a revised description of consented
fields. **Create new** when anything about the subject's identity needs to
differ: identity fields are immutable, so a corrected SSN, a legal name
change, or a new email all require a fresh record and a switch to its
`consent_id`.
The `consent_id` requirement documented here applies to **queries** —
reading data back out of the network. See
[Furnishing](/api-overview/furnishing/overview) for the obligations that apply when
contributing data.
## In the dashboard
## Related concepts
The subjects consent records are about, and how to find them.
How consent plugs into a product query.
What determines which fields a query returns.
The error envelope and status codes used across the API.
# Entities
Source: https://docs.solo.one/concepts/identity/entities
Consumers and businesses — the subjects every query and furnish is about
An **entity** is the subject of a verification: the person or organization you
are furnishing data about or querying data for. Every furnish and every query in
the SOLO Network is about exactly one entity — you never operate on data in the
abstract, only in the context of a specific consumer or business.
Individual people, identified by personal details such as name, date of birth, email, phone, and SSN.
Legal entities, identified by details such as legal name, jurisdiction of formation, registration number, and tax identifier.
The same person or company resolves to a **single profile** in a network. When
you furnish or query, the network matches the identity you supply to that
profile, so contributions from different participants accumulate on one subject
rather than fragmenting into duplicates.
## How entities come into existence
You never call a "create entity" endpoint. Profiles materialize as a side effect
of normal participation:
When you [furnish](/api-overview/furnishing/overview) through a product endpoint
(for example `POST /v1/products/kyc_certificate/furnish`), the identity in
your payload is resolved to an existing profile or a new one is created.
When you create a [consent](/concepts/identity/consent) record with
`POST /v1/consent/consumer`, the identity payload is used to create and link
a consumer profile if you don't point at an existing one.
Consent creation accepts an optional `consumer_id`. If you've already found
the subject via search, pass its `id` and the consent links to that profile
instead of creating a new one.
## Consumers
A consumer represents a single individual, identified by:
| Field | Notes |
| ------------------------- | --------------------------------- |
| `first_name`, `last_name` | Legal name |
| `date_of_birth` | ISO `YYYY-MM-DD` |
| `personal_email` | |
| `phone_number` | |
| `social_security_number` | Used for high-confidence matching |
You don't need every field to identify a consumer, but more identifiers produce
higher-confidence matches. SSN and date of birth are the strongest signals.
## Businesses
A business represents a legal entity, identified by:
| Field | Notes |
| ----------------------------------------------------------------- | --------------------------------------- |
| `business_legal_name` | Registered legal name |
| `business_jurisdiction_of_formation` | e.g. a US state |
| `business_registration_identifier_from_jurisdiction_of_formation` | Registration number issued at formation |
| `business_tax_identifier_type` / `business_tax_identifier_value` | e.g. `EIN` |
| `business_dba_name` | Optional "doing business as" name |
| `business_website_url` | Optional |
## Searching for entities
Before furnishing or querying, look up subjects that already exist with the
search endpoints:
* `GET /v1/entities/consumer/search`
* `GET /v1/entities/business/search`
Both endpoints share the same mechanics:
* **`network_id` is required.** Search is always scoped to a single
[network](/concepts/governance/networks); you cannot search across networks in one call.
* **All identity parameters are optional** and combine with AND — every
criterion you supply must match.
* **`limit`** caps results between 1 and 100, defaulting to 20.
### Match semantics
Each search parameter has a fixed matching rule:
| Kind | Parameter | Match |
| -------- | ------------------------ | ------------------------ |
| Consumer | `first_name` | Case-insensitive partial |
| Consumer | `last_name` | Case-insensitive partial |
| Consumer | `personal_email` | Case-insensitive partial |
| Consumer | `social_security_number` | Exact |
| Consumer | `date_of_birth` | Exact (`YYYY-MM-DD`) |
| Business | `business_legal_name` | Case-insensitive partial |
| Business | `business_dba_name` | Case-insensitive partial |
| Business | `business_email` | Case-insensitive partial |
| Business | `federal_ein` | Exact (integer) |
Partial matching means `first_name=jan` matches both "Jan" and "Janet";
exact-match fields like SSN and EIN must be supplied in full.
### Searching consumers
```bash theme={null}
curl -G https://api.solo.one/v1/entities/consumer/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…" \
--data-urlencode "first_name=Jane" \
--data-urlencode "last_name=Doe" \
--data-urlencode "limit=10"
```
A `200 OK` returns an array of matching profiles:
```json theme={null}
[
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"identifier": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-05-02T18:21:44.310000+00:00",
"updated_at": "2026-06-01T09:03:12.778000+00:00",
"first_name": "Jane",
"last_name": "Doe",
"personal_email": "jane@example.com",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
```
Consumer results carry these fields:
| Field | Type | Notes |
| ------------------------ | -------------- | --------------------------------- |
| `id` | UUID | The consumer profile's identifier |
| `identifier` | string | Same value as `id` |
| `created_at` | string \| null | When the profile was created |
| `updated_at` | string \| null | When the profile last changed |
| `first_name` | string \| null | |
| `last_name` | string \| null | |
| `personal_email` | string \| null | |
| `date_of_birth` | string \| null | `YYYY-MM-DD` |
| `social_security_number` | string \| null | |
### Searching businesses
```bash theme={null}
curl -G https://api.solo.one/v1/entities/business/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…" \
--data-urlencode "business_legal_name=Acme" \
--data-urlencode "limit=10"
```
```json theme={null}
[
{
"id": "b4d2e9a1-0f6c-4a3e-8d57-91c2f0ab34de",
"identifier": "b4d2e9a1-0f6c-4a3e-8d57-91c2f0ab34de",
"created_at": "2026-04-18T11:02:09.114000+00:00",
"updated_at": "2026-05-30T16:47:51.902000+00:00",
"business_legal_name": "Acme Corp",
"business_dba_name": "Acme",
"business_email": "ops@acme.example.com",
"business_entity_type": "corporation",
"business_date_of_incorporation": "2015-03-09",
"business_phone": "+14155550123",
"business_website_url": "https://acme.example.com",
"federal_ein": 123456789,
"employee_count": 250
}
]
```
Business results carry these fields:
| Field | Type | Notes |
| -------------------------------- | --------------- | --------------------------------- |
| `id` | UUID | The business profile's identifier |
| `identifier` | string | Same value as `id` |
| `created_at` | string \| null | When the profile was created |
| `updated_at` | string \| null | When the profile last changed |
| `business_legal_name` | string \| null | |
| `business_dba_name` | string \| null | |
| `business_email` | string \| null | |
| `business_entity_type` | string \| null | e.g. corporation, LLC |
| `business_date_of_incorporation` | string \| null | |
| `business_phone` | string \| null | |
| `business_website_url` | string \| null | |
| `federal_ein` | integer \| null | |
| `employee_count` | number \| null | |
Search returns **core identity fields only** — enough to confirm you have the
right subject. It is not a data product: credit and verification data come
back through [product queries](/api-overview/querying/overview), gated by consent and
entitlement.
## From search to consent: find or create
The search endpoints exist primarily to support a **find-or-create** workflow
before consenting and querying a consumer:
Call `GET /v1/entities/consumer/search` with the strongest identifiers you
hold (SSN and date of birth where possible).
Pass the result's `id` as `consumer_id` when calling
`POST /v1/consent/consumer`. The consent attaches to the existing profile,
keeping the subject's history consolidated.
Omit `consumer_id`. The identity payload on the consent request creates and
links a fresh consumer profile.
Use the returned `consent_id` on [product queries](/api-overview/querying/products) for
that subject.
See [Consent](/concepts/identity/consent) for the full consent API.
## Provenance: one entity, many furnishers
An entity profile is a **shared subject**, not a record you own. In a healthy
network, several participants typically furnish data about the same consumer or
business — one bank contributes identity verification outcomes, another
contributes fraud events, a third contributes business registration details.
All of it accrues to the same profile.
Two consequences follow:
1. **You don't control the whole profile.** Your furnished fields sit alongside
fields contributed by other participants. Updating your own contribution
never overwrites theirs.
2. **What you can read is governed, not automatic.** Sharing a profile with
another furnisher does not mean you see their data. A query returns the
intersection of what the network's
[querying policy](/concepts/governance/querying-policies) exposes and what you are
[entitled](/concepts/governance/entitlement) to — entitlement you earn by furnishing or
previously querying that entity.
Identifying an entity tells the network *who* you mean. Consent and entitlement
determine *what* you may read about them. The same search result can yield
very different query responses for two different participants.
For how each furnisher's contribution stays attributed — and how the network
chooses which furnisher's verification to reuse — see
[Provenance](/concepts/trust/provenance).
## Network scoping
Entities are always handled in the context of a network:
* Search requires a `network_id` and only surfaces subjects visible to you in
that network.
* Consent records are created against a `network_id`, and the same subject
needs separate consent per network.
* Furnishes and queries run under a network's policies.
If you participate in multiple networks, treat each as a separate scope — an
entity you can see in one network is not automatically visible in another. See
[Networks](/concepts/governance/networks) for how membership and roles work.
## Common questions
No profile visible to you in that network matched **all** of your criteria.
Remember that criteria combine with AND: `first_name=Jane` plus
`last_name=Smith` only matches profiles satisfying both. Try fewer or
looser criteria, or double-check exact-match fields (`social_security_number`,
`date_of_birth`, `federal_ein`) for typos — partial input never matches an
exact-match field.
Partial matching on names and emails can surface several candidates.
Disambiguate with the exact-match identifiers — SSN or date of birth for
consumers, federal EIN for businesses — before linking a profile to a
consent record. When in doubt, omit `consumer_id` and let consent creation
work from the identity payload you trust.
Yes. All identity parameters are optional, so a request with only
`network_id` returns profiles up to `limit` (default 20, max 100). This is
useful for an initial look at a network, but for production matching always
pass the strongest identifiers you hold.
## In the dashboard
## Related concepts
How to record permission to query a consumer or business.
Why you can read a given field about an entity.
How contributing data creates and enriches entities.
How consolidated entity data is read back.
# Business graph
Source: https://docs.solo.one/concepts/trust/business-graph
What can actually be verified about a business in the network, and how each branch becomes a reusable trust asset
The **business graph** is everything a SOLO network can hold about a legal
entity: a core identity, plus the verification events participants have furnished
about it. Each branch is a candidate [trust asset](/concepts/trust/trust-assets)
— an attested verification another institution can reuse instead of repeating
registry lookups, ownership tracing, and risk screening.
This page shows what lives in the graph **today**. Every node maps to a real
model; branches with no backing model in the current catalog (for example
business financials/revenue, banking relationships, or tax-return inventories)
are deliberately absent rather than implied.
## The graph
```text theme={null}
Business
├── Identity (business)
│ ├── business_legal_name, business_dba_name
│ ├── business_jurisdiction_of_formation
│ ├── business_entity_type
│ ├── business_date_of_incorporation
│ ├── federal_ein / tax identifier
│ ├── business_website_url, business_email, business_phone
│ └── employee_count
│
├── Business identity verification (business_identity_verification_event)
│ ├── registration_status, registration_identifier
│ ├── jurisdiction_of_formation, entity_type
│ ├── operational_existence_status
│ ├── tax_id_validation_result, address_verification_result
│ ├── is_secretary_of_state_match
│ └── formation_document_artifact (evidence)
│
├── Ownership & control (business_ownership_control_verification_event)
│ ├── beneficial_owners_identified_count
│ ├── control_persons_identified_count
│ ├── authorized_representatives_identified_count
│ ├── beneficial_ownership_determination_method
│ ├── beneficial_ownership_threshold_applied
│ ├── control_determination_basis
│ └── ownership_evidence_artifact (evidence)
│
├── Risk & compliance (business_risk_compliance_event)
│ ├── sanctions_screening_result
│ ├── adverse_media_assessment_result
│ ├── is_on_aml_watchlist, is_on_ofac_watchlist
│ ├── activity_risk_level
│ ├── restricted_activity_assessment_result
│ └── primary_activity_classification_code / system
│
└── Consent (business consent)
└── scope, expires_at, consented_fields
```
## How to read the graph
* **Identity** is the [entity](/concepts/identity/entities) itself — the legal
name, jurisdiction, and tax identifier that let contributions from different
participants accrue to one business.
* **Verification branches** are furnished events. A
[KYB certificate](/api-overview/querying/kyb-certificate) query consolidates the
three verification branches — identity, ownership & control, and
risk & compliance — into one certificate.
* **Registration and addresses** are not separate branches; they are fields
inside business identity verification (`registration_status`,
`business_registered_address`, `business_operating_address`).
* **Consent** is the permission layer that governs reading any branch.
## Each branch carries provenance
Every populated KYB sub-product carries the `furnishing_entity_id` and
`attestation_id` of the participant whose data backed it, plus an `assertions`
block recording what was asserted and when. Ownership and identity branches
reference concrete evidence artifacts (a cap table, articles of organization).
See [Provenance](/concepts/trust/provenance) for the full lineage model.
What you can actually read back is the intersection of the network's
[querying policy](/concepts/governance/querying-policies) and your
[entitlement](/concepts/governance/entitlement). The graph describes what
*can* exist; consent, policy, and entitlement decide what *you* see.
## Reusable trust assets for a business
Consolidated through products, the business graph yields reusable assets such
as:
* **Verified business identity** — registration, jurisdiction, and tax-ID
validation from a [KYB certificate](/api-overview/querying/kyb-certificate).
* **Verified ownership** — beneficial owners and control persons, evidenced.
* **Risk & compliance screening** — sanctions, adverse media, and watchlist
outcomes.
## Related
The equivalent for consumers.
The consolidated business trust asset, field by field.
What makes a verification reusable.
The lineage each branch carries.
# Consumer graph
Source: https://docs.solo.one/concepts/trust/consumer-graph
What can actually be verified about a consumer in the network, and how each branch becomes a reusable trust asset
The **consumer graph** is everything a SOLO network can hold about a person: a
core identity, plus the verification events participants have furnished about
them. Each branch is a candidate [trust asset](/concepts/trust/trust-assets) —
an attested verification another institution can reuse instead of repeating.
This page shows what lives in the graph **today**. Every node maps to a real
model; branches with no backing model (for example consumer assets, liabilities,
or bank-account inventories) are deliberately absent rather than implied.
## The graph
```text theme={null}
Consumer
├── Identity (consumer)
│ ├── first_name, last_name
│ ├── date_of_birth
│ ├── personal_email
│ ├── phone_number
│ └── social_security_number
│
├── Identity verification — KYC (KYC certificate sub-products)
│ ├── Document capture & review (document_capture_event, identity_document)
│ ├── Biometric capture & review (biometric_capture_event)
│ ├── Liveness capture & review (liveness_check_event)
│ ├── Address capture & verification (address_capture_event)
│ └── Identity corroboration (identity_verification_event)
│ └── is_ssn_match, is_name_match, is_dob_match, kyc_decision
│
├── Address verification (address_verification)
│ └── is_match, address_match_quality, address_verification_method_type
│
├── Income (income_verification_event)
│ ├── base_salary, monthly_income
│ ├── bonus_commission_income, overtime_income
│ ├── debt_to_income_ratio
│ └── paystub_upload (evidence)
│
├── Employment (employment_verification_event)
│ ├── employer_name, job_title
│ ├── employment_status, employment_type
│ ├── employment_start_date, length_of_employment_months
│ └── employer_letter_of_employment_upload (evidence)
│
├── Phone (phone_number_verification)
│ └── phone_carrier_name, phone_age_days, phone_risk_score
│
├── Fraud signals (fraud_verification_event)
│ ├── confirmed_fraud_indicator
│ ├── fraud_attribute_label, fraud_loss_event_category
│ └── fraud_event_date
│
└── Consent (consumer consent)
└── scope, expires_at, consented_fields
```
## How to read the graph
* **Identity** is the [entity](/concepts/identity/entities) itself — the
matching keys (SSN, date of birth) that let contributions from different
participants accrue to one person.
* **Verification branches** are furnished events. The richest is **KYC**, which
a [KYC certificate](/api-overview/querying/kyc-certificate) query consolidates
into up to nine sub-products. Income, employment, phone, and fraud are
separate verification-event models that can also be furnished about a
consumer.
* **Consent** is not data *about* the consumer so much as the permission layer
that governs reading any of it.
## Each branch carries provenance
A branch only becomes a reusable trust asset because its lineage travels with
it. Certificate sub-products always carry the `furnishing_entity_id` and
`attestation_id` of the participant whose data backed them, plus an `assertions`
block recording what was asserted and when. Evidence is concrete — a paystub
upload behind an income event, a captured document behind identity
verification. See [Provenance](/concepts/trust/provenance) for the full lineage
model.
What you can actually read back is the intersection of the network's
[querying policy](/concepts/governance/querying-policies) and your
[entitlement](/concepts/governance/entitlement). The graph describes what
*can* exist; consent, policy, and entitlement decide what *you* see.
## Reusable trust assets for a consumer
Consolidated through products, the consumer graph yields reusable assets such
as:
* **Verified identity** — a [KYC certificate](/api-overview/querying/kyc-certificate)
consolidating document, biometric, liveness, address, and corroboration
evidence.
* **Verified address** — address capture and verification outcomes.
* **Fraud screening** — [screening list](/api-overview/querying/screening-lists)
results drawn from furnished fraud signals.
## Related
The equivalent for businesses.
The consolidated identity trust asset, field by field.
What makes a verification reusable.
The lineage each branch carries.
# Provenance
Source: https://docs.solo.one/concepts/trust/provenance
Who verified a fact, how, when, and on what evidence — the lineage that makes reuse safe
Provenance is what separates a SOLO network from a black box. Banks do not want
to trust SOLO; they want to trust the **institution that did the verification**,
and to see the evidence. So every reusable [trust asset](/concepts/trust/trust-assets)
carries its lineage: who contributed it, who attested to it, when, by what
method, and on what evidence.
> SOLO does not ask participants to trust unattributed data. It preserves the
> lineage of every reusable verification.
## The lineage every certificate carries
When a [product query](/api-overview/querying/products) consolidates verifications
into a certificate, each populated sub-product is self-describing:
```json theme={null}
"business_identity_verification": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"business_identity_verification_assertion": true,
"business_identity_verification_performed_timestamp": "2026-05-02T00:00:00Z"
},
"data": {
"business_formation_document_artifact": "articles_of_organization.pdf",
"business_identity_verification_sources": ["state_registry"],
"business_tax_id_validation_result": "match"
}
}
```
| Lineage element | Field | What it tells the reader |
| -------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------- |
| **Contributor / verifier** | `furnishing_entity_id` | Which participant furnished and stands behind this verification |
| **Attestation** | `attestation_id` | The attestation record linking that participant to the asserted facts |
| **Assertion + time** | `assertions.*` and its `…_timestamp` | What was asserted, and when the verification was performed |
| **Method & sources** | `data.*_sources`, `*_method`, `*_result` | How it was verified (e.g. `state_registry`, `automated_ocr`, `match`) |
| **Evidence** | artifact/upload fields (e.g. `…_artifact`, `paystub_upload`) | The concrete document behind the assertion |
| **As-of date** | the certificate's `certificate_as_of_date` | How current the consolidated result is |
The [coverage check](/api-overview/querying/coverage-check) goes a step further and
returns a human-readable `furnishing_entity_name` alongside the
`furnishing_entity_id`, so you can see *which banks* can satisfy a product for a
subject before you query.
## One subject, many furnishers
An entity profile is a **shared subject**, not a record any one participant
owns. In a healthy network several participants furnish about the same consumer
or business — one contributes identity verification, another fraud signals, a
third business registration. All of it accrues to the same profile, and each
contribution keeps its own attribution.
Two consequences follow:
1. **You don't own the whole profile.** Your furnished fields sit alongside
others'. Updating your contribution never overwrites theirs.
2. **What you can read is governed, not automatic.** Sharing a subject with
another furnisher does not mean you see their data — a query returns the
intersection of the [querying policy](/concepts/governance/querying-policies)
and your [entitlement](/concepts/governance/entitlement).
## How the network picks which furnisher's verification to reuse
When multiple furnishers have verified the same thing, the query does not guess.
The querying policy defines **provenance options** — ordered, filtered
preferences for which furnished events qualify and in what priority. At
resolution time the network applies the policy's filters and selects the
**oldest matching event** for each sub-product across the allowed networks,
falling back through the policy's priority order until a qualifying verification
is found.
```mermaid theme={null}
flowchart TB
Q[Query] --> Gather["Gather furnished events across network_ids"]
Gather --> Filter["Apply policy provenance options + freshness filters"]
Filter --> Pick["Select qualifying event per sub-product"]
Pick --> Decision{All required sub-products satisfied?}
Decision -- yes --> Cert["Issue certificate · 200, with per-source attribution"]
Decision -- no --> Empty["204 No Content"]
```
You can also narrow reuse yourself: pass `furnishing_entity_ids` on a query to
consider only specific contributors.
## Freshness is part of provenance
A verification is only reusable while it is current enough for the reader's
policy. Querying policies enforce **freshness windows** (for example, "identity
corroboration within the last 30 days") using certificate-level fields such as
`days_since_certificate_as_of_date`. Data outside the window is not reused — the
query returns `204` rather than handing back a stale answer dressed up as fresh.
This is why freshness, like attribution, is a first-class part of what makes
reuse safe.
## Why provenance is exposed on purpose
For most software, exposing internals is a mistake. For SOLO it is the product:
participants reuse each other's work *because* they can see who did it, how, and
when. Provenance turns "trust SOLO" into "trust the network, transparently."
Provenance here means the lineage that exists today — `furnishing_entity_id`,
`attestation_id`, assertion timestamps, source/method/evidence fields, and
policy freshness. It does not include reuse counters, dispute counts, or
contributor reputation scores, which are not part of the current platform.
## Related
The reusable unit provenance describes.
Why you can read a given field about a subject.
Freshness windows and provenance options.
See which furnishers can satisfy a product before you query.
# Trust assets
Source: https://docs.solo.one/concepts/trust/trust-assets
The reusable unit of the network — an attested, evidenced verification that another institution can safely build on
A **trust asset** is the reusable unit of a SOLO network: a verification that
one institution completed, backed by evidence, attributed to its source, and
governed by permission — so that another institution can reuse it instead of
repeating the work.
"Trust asset" is a lens, not a new API object. It names what already exists in
the platform when you put the pieces together: a furnished
[verification](/api-overview/furnishing/overview) event, its supporting evidence,
the [attestation](/concepts/trust/provenance) that records who stands behind it,
and the [consent](/concepts/identity/consent) and
[entitlement](/concepts/governance/entitlement) that decide who may reuse it. A
[product query](/api-overview/querying/products) assembles trust assets into a
certificate.
## What makes a verification a *trust asset*
A raw data point — `revenue: $5.2M` — is not reusable on its own. Another bank
cannot act on a number with no idea who produced it, how, or when. A trust asset
carries the context that makes reuse safe:
| Dimension | The trust question | Where it lives in the API |
| --------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Subject** | Who is this about? | The [entity](/concepts/identity/entities) (consumer or business) |
| **Assertion** | What was attested, and when? | The verification event's `assertions` block (e.g. `business_identity_verification_assertion`, with its `…_timestamp`) |
| **Evidence** | Why should anyone believe it? | The verification event's `data` block and artifact fields (e.g. `business_formation_document_artifact`) |
| **Attribution** | Who verified it? | `furnishing_entity_id` + `attestation_id` on every populated sub-product |
| **Permission** | Who is allowed to reuse it? | The subject's [consent](/concepts/identity/consent) |
| **Precision** | Which fields may *this* reader see? | [Entitlement](/concepts/governance/entitlement), earned by participation |
| **Reuse eligibility** | Is it fresh and complete enough to count? | The [querying policy](/concepts/governance/querying-policies)'s field and freshness filters |
| **Consolidation** | How is it delivered for reuse? | A [certificate](/api-overview/querying/products) (`certificate_id`, with its as-of date) |
A data aggregator returns the first row of that table. SOLO returns all of it.
## A trust asset, concretely
Here is one populated sub-product from a [KYB certificate](/api-overview/querying/kyb-certificate)
query — a single trust asset in the wild:
```json theme={null}
"business_identity_verification": {
"furnishing_entity_id": "e1d2c3b4-…",
"attestation_id": "f0a1b2c3-…",
"assertions": {
"business_identity_verification_assertion": true,
"business_identity_verification_performed_timestamp": "2026-05-02T00:00:00Z"
},
"data": {
"business_formation_document_artifact": "articles_of_organization.pdf",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier": "7423918",
"business_registration_status": "active",
"business_identity_verification_sources": ["state_registry"],
"business_tax_id_validation_result": "match"
}
}
```
Read it as a trust asset:
* **Subject** — the business the certificate was issued for (`business_id` on
the enclosing response).
* **Assertion** — business identity *was* verified, on `2026-05-02`.
* **Evidence** — backed by an articles-of-organization artifact, a state
registry source, and a tax-ID match.
* **Attribution** — furnished and attested by the participant identified by
`furnishing_entity_id` / `attestation_id`.
* **Reuse** — surfaced to you only because consent permitted the read, your
entitlement covered these fields, and the data passed the policy's freshness
window.
The next institution onboarding this business does not re-pull the registry or
re-collect the formation document. It queries the certificate and reuses this
asset — while the contributor's attribution travels with it.
## How trust assets are created and reused
```mermaid theme={null}
flowchart LR
Inst["Institution verifies a subject"] --> Furn["Furnish verification + evidence"]
Furn --> Asset[("Trust asset: assertion + evidence + attestation")]
Asset --> Policy["Querying policy: fresh + complete enough?"]
Consent["Subject consent"] --> Policy
Ent["Reader entitlement"] --> Policy
Policy --> Cert["Certificate issued · reused by another institution"]
```
1. **Create.** An institution [furnishes](/api-overview/furnishing/overview) the
verification it performed. The furnished events, their evidence, and the
institution's attestation become a trust asset attributed to that
contributor.
2. **Govern.** [Consent](/concepts/identity/consent) decides whether the asset
can be read for a subject; [entitlement](/concepts/governance/entitlement)
decides which fields a given reader sees; the
[querying policy](/concepts/governance/querying-policies) decides whether the
asset is fresh and complete enough to count.
3. **Reuse.** A [product query](/api-overview/querying/products) consolidates the
eligible trust assets into a [certificate](/api-overview/querying/kyc-certificate).
If nothing eligible exists, the query returns `204` rather than a stale or
unattributed answer.
## The three layers
Trust assets sit in the middle of how the platform is organized:
| Layer | What it is | Pages |
| ------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Trust graph** | The subjects and the verifications attached to them | [Consumer graph](/concepts/trust/consumer-graph), [Business graph](/concepts/trust/business-graph) |
| **Trust assets** | Attested, evidenced verifications that become reusable | This page, [Provenance](/concepts/trust/provenance) |
| **Trust products** | The outcomes a query delivers | [Products](/api-overview/querying/products), [KYC](/api-overview/querying/kyc-certificate), [KYB](/api-overview/querying/kyb-certificate) |
Trust assets describe data that **exists today** — furnished verification
events, their attestations, and the certificates that consolidate them. SOLO
does not expose a standalone `TrustAsset` object, reuse counters, or
verification "tiers"; the reusable asset is the furnished, attested
verification itself, surfaced through certificate queries.
## Related
The lineage every trust asset carries.
The trust assets that can exist for a business.
The trust assets that can exist for a consumer.
How trust assets are consolidated and delivered.
# Authentication
Source: https://docs.solo.one/home/authentication
Authenticate with the SOLO Network API using WorkOS-issued bearer JWTs verified over JWKS
Every public endpoint under `/v1` requires a bearer token in the
`Authorization` header. The token is a JWT issued by **WorkOS**, SOLO's
identity provider, when you sign in to the SOLO dashboard. The API verifies it
cryptographically on every request — there are no API keys and no shared
secrets to manage on your side.
```bash theme={null}
Authorization: Bearer
```
Only `GET /health` and `GET /version` are exempt; everything else returns
`401 Unauthorized` without a valid token.
## Getting a token
Access to the API starts with your organization being provisioned by SOLO.
Once that's done, tokens are minted through authentication with the SOLO
dashboard.
SOLO onboards your organization into WorkOS and associates it with your
entity on the network. If you don't have access yet, contact your SOLO
account manager.
Authenticate through the SOLO dashboard sign-in flow. On success, WorkOS
issues an access token — a JWT signed with RS256 — bound to your user and
organization.
Pass the access token in the `Authorization` header using the `Bearer`
scheme. The same token works for both the dashboard and direct API calls
against that environment.
Treat tokens as secrets. Never commit them to source control, embed them in
client-side code you don't control, or log them. Anyone holding the token can
act as your organization until it expires.
## What the server verifies
When a request arrives, the API verifies the token before any handler runs:
1. **Signature.** The token's key ID (`kid`) is looked up in WorkOS's published
JWKS endpoint (`https://api.workos.com/sso/jwks/`), and the
RS256 signature is verified against that public key. Tokens signed by
anything other than WorkOS — or minted for a different environment's client
— fail here, because their signing key isn't in the key set.
2. **Standard claims.** The decoded payload is validated, including expiry
(`exp`). An expired token is rejected.
3. **Identity resolution.** The verified claims (your user ID, organization,
and role) are used to resolve your account and entity on the network. If
the token is valid but no matching account exists, the request is rejected
with a 401.
Endpoints that demand specific permissions additionally check the token's
`permissions` (or `role`) claim; a valid token without the required permission
receives a `403 Forbidden`.
Verification happens against WorkOS's public keys — the API never sees or
stores your password, and tokens cannot be forged without WorkOS's private
key.
## Token lifetime and refresh
WorkOS access tokens are short-lived. Don't cache one and reuse it
indefinitely:
* **Refresh proactively.** Re-authenticate (or use your sign-in session's
refresh mechanism) before the token's `exp` claim passes, rather than waiting
for failures.
* **Handle 401 as a refresh signal.** If a previously working request starts
returning `401` with `"Invalid or expired token"`, obtain a fresh token and
retry the request once. Do not retry in a loop with the same token.
* **Don't share tokens across environments.** A token is bound to one
environment's WorkOS client. Sandbox tokens fail signature lookup in
production and vice versa.
## Environments
Use the base URL for the environment your credentials were issued in:
| Environment | Base URL |
| ----------- | ------------------------------ |
| Development | `https://api.dev.solo.one` |
| Sandbox | `https://api.sandbox.solo.one` |
| Production | `https://api.prod.solo.one` |
## Example request
A complete authenticated call — searching for a consumer by name within a
network using `GET /v1/entities/consumer/search`:
```bash theme={null}
curl -G "https://api.sandbox.solo.one/v1/entities/consumer/search" \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=2f6f6a3e-9d6b-4a4e-8a6f-3a1d1f2b9c10" \
--data-urlencode "first_name=Ada" \
--data-urlencode "last_name=Lovelace" \
--data-urlencode "limit=10"
```
A successful response returns matching consumer identities:
```json theme={null}
[
{
"id": "7c0b6e2a-1f4d-4f7e-9b2a-5e8c3d1a6f90",
"identifier": "consumer-7c0b6e2a",
"first_name": "Ada",
"last_name": "Lovelace",
"personal_email": "ada@example.com",
"date_of_birth": "1990-12-10",
"created_at": "2026-05-01T12:00:00Z"
}
]
```
The same header works on every endpoint — swap the path and method:
```bash theme={null}
curl -X POST "https://api.sandbox.solo.one/v1/consent/consumer" \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ ... }'
```
## 401 vs 403
The two failure modes mean different things, and you should handle them
differently.
**`401 Unauthorized` — the API doesn't know who you are.** The token is
missing, malformed, expired, or doesn't resolve to an account. Fix the token
and retry.
```json theme={null}
{ "detail": "Requires authentication" }
```
```json theme={null}
{ "detail": "Invalid or expired token" }
```
```json theme={null}
{ "detail": "Could not validate credentials or find account" }
```
**`403 Forbidden` — the API knows who you are, and the answer is no.** The
token verified successfully, but it lacks a required permission, or your
account isn't allowed to perform this operation on this resource. A retry with
the same credentials will fail the same way; this is a provisioning or
authorization problem, not a token problem.
```json theme={null}
{ "detail": "Not enough permissions" }
```
Domain-level authorization failures (for example, operating on a network you
don't belong to) also return `403`, with the full error envelope described in
[Errors](/home/errors):
```json theme={null}
{
"detail": "Permission denied",
"error_code": "PERMISSION_DENIED",
"request_id": "a1b2c3d4-..."
}
```
Rule of thumb: on `401`, refresh the token and retry once. On `403`, stop and
check your roles and network membership — retrying won't help.
## Troubleshooting
| Symptom | Status and body | Cause | Fix |
| ------------------------------------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- |
| No `Authorization` header sent | `401` — `"Requires authentication"` | Header missing entirely | Add `Authorization: Bearer $SOLO_TOKEN` to the request |
| Wrong scheme (e.g. `Authorization: Token …` or a bare token) | `401` — `"Requires authentication"` | The header isn't a valid `Bearer` credential, so it's treated as absent | Use exactly `Bearer ` — capital B, single space |
| Expired token | `401` — `"Invalid or expired token"` | The `exp` claim has passed | Obtain a fresh token and retry once |
| Token from the wrong environment | `401` — `"Invalid or expired token"` | The signing key isn't in this environment's JWKS | Use a token issued for the environment you're calling |
| Garbled or truncated token | `401` — `"Invalid or expired token"` | The JWT can't be decoded or its key can't be resolved | Re-copy the token; check for whitespace or truncation in your env var |
| Valid token, unknown account | `401` — `"Could not validate credentials or find account"` | Your user verified but no account/entity exists for it yet | Usually first-time setup — contact SOLO support |
| Valid token, missing permission | `403` — `"Not enough permissions"` | The `permissions`/`role` claim lacks what the endpoint requires | Ask your administrator to update your role |
| Persistent `500` — `"Authentication service is unavailable"` | `500` | Server-side identity configuration problem (not your token) | Report to SOLO support with your `request_id` |
If you're stuck, capture the full response body — including the `request_id`
when present — and quote it to support. See
[Errors](/home/errors#request-ids) for how request IDs are correlated.
## Best practices
Make API calls from your backend and keep tokens out of code you don't
control. A bearer token carries your organization's full standing on the
network for as long as it's valid.
The examples on this site use `$SOLO_TOKEN` for a reason: keeping the token
in an environment variable (or a secrets manager) keeps it out of shell
history, scripts, and version control.
Because tokens are short-lived, an integration that fetches one token at
startup will start failing mid-run. Centralize token acquisition behind a
helper that refreshes on expiry, and treat a `401` anywhere as a signal to
refresh once and retry.
Request only the roles and permissions your integration needs. A
furnish-only service doesn't need querying permissions — limiting the
token's reach limits the blast radius if it leaks.
# Errors
Source: https://docs.solo.one/home/errors
The error response envelope, every HTTP status the API returns, and how to handle each one
The SOLO Network API uses standard HTTP status codes and returns structured
JSON error bodies. This page documents the exact response shapes, real examples
of each status, and what your integration should do in response.
## The error envelope
Application errors — anything the API's domain logic rejects — return a
consistent envelope:
```json theme={null}
{
"detail": "Consent record not found",
"error_code": "RESOURCE_NOT_FOUND",
"request_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
}
```
| Field | Type | Presence | Meaning |
| ------------ | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `detail` | string | always | Human-readable description of what went wrong. Safe to log and show to operators; not guaranteed stable, so don't branch on its text. |
| `error_code` | string | always | Machine-readable category (see table below). Stable — branch on this. |
| `request_id` | string | when available | Unique ID for this request, assigned by the API. Quote it to support. |
| `errors` | array | occasionally | Itemized sub-errors for operations that validate many things at once (for example, rows in a file upload). |
Two kinds of responses use a **shorter shape** — just `{"detail": "..."}` with
no `error_code`:
* **`422` request-shape validation**, where FastAPI rejects the request before
it reaches domain logic.
* **Authentication-layer `401`/`403`**, raised while verifying the bearer
token (see [Authentication](/home/authentication#401-vs-403)).
```json theme={null}
{ "detail": "body -> first_name: Field required" }
```
### Error codes
| `error_code` | Status | Raised when |
| ------------------------- | ------ | ------------------------------------------------------------------------- |
| `VALIDATION_ERROR` | `400` | The request was well-formed but violates a domain rule |
| `AUTHENTICATION_REQUIRED` | `401` | Domain logic could not establish who you are |
| `PERMISSION_DENIED` | `403` | You're authenticated but not allowed to do this |
| `RESOURCE_NOT_FOUND` | `404` | The referenced resource doesn't exist (or isn't visible to you) |
| `RESOURCE_CONFLICT` | `409` | The operation conflicts with existing state (duplicate, version mismatch) |
| `OPERATION_FAILED` | `500` | A known internal operation failed |
| `INTERNAL_ERROR` | `500` | An unexpected, unhandled error |
## HTTP status codes
| Code | Meaning on this API |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200 OK` | Success. The body contains the requested data. |
| `204 No Content` | A product query resolved successfully but produced **no result** — the available data did not satisfy the policy's requirements. The body is empty; the `X-Ref-Id` response header carries the query's reference ID. |
| `400 Bad Request` | Domain validation failed: the request was structurally valid but violates a business rule (e.g. consent not gathered, no updatable fields provided). |
| `401 Unauthorized` | Missing, malformed, expired, or unresolvable bearer token. |
| `403 Forbidden` | Valid token, but the operation isn't permitted for your account or role. |
| `404 Not Found` | The resource doesn't exist within your network scope. |
| `409 Conflict` | The resource already exists or the operation conflicts with current state. |
| `422 Unprocessable Entity` | Request-shape validation failed: a missing, mistyped, or malformed field. The `detail` string names the field path. |
| `500 Internal Server Error` | Something went wrong on SOLO's side. Logged and monitored automatically. |
A `204` is not an error. It means the query ran — and is accounted for —
but the network could not assemble a result that meets the policy's
requirements for this entity. Capture the `X-Ref-Id` header before treating
the response as "no data": it's the reference for that specific query if you
need to follow up.
## Examples by status
Each example below is a real response produced by the API's handlers.
Creating a consumer consent without affirming consent was gathered:
```json theme={null}
{
"detail": "Consent cannot be created without prior consumer consent gathering",
"error_code": "VALIDATION_ERROR",
"request_id": "a1b2c3d4-..."
}
```
Updating a consent record with an empty change set:
```json theme={null}
{
"detail": "At least one of scope, expires_at, or consented_fields must be provided",
"error_code": "VALIDATION_ERROR",
"request_id": "a1b2c3d4-..."
}
```
Querying a product without identifying the subject:
```json theme={null}
{
"detail": "Either consent_id or consumer_id must be provided",
"error_code": "VALIDATION_ERROR",
"request_id": "a1b2c3d4-..."
}
```
Authentication-layer rejections use the short shape:
```json theme={null}
{ "detail": "Requires authentication" }
```
```json theme={null}
{ "detail": "Invalid or expired token" }
```
A valid token whose user has no account on the network yet:
```json theme={null}
{ "detail": "Could not validate credentials or find account" }
```
A token lacking a required permission (short shape, from the auth layer):
```json theme={null}
{ "detail": "Not enough permissions" }
```
A domain-level authorization failure (full envelope):
```json theme={null}
{
"detail": "Permission denied",
"error_code": "PERMISSION_DENIED",
"request_id": "a1b2c3d4-..."
}
```
Reading a consent that doesn't exist in the given network scope:
```json theme={null}
{
"detail": "Consent record not found",
"error_code": "RESOURCE_NOT_FOUND",
"request_id": "a1b2c3d4-..."
}
```
Configuring a furnishing policy by an unknown ID:
```json theme={null}
{
"detail": "FurnishingPolicy with id 4f6e2a90-... not found",
"error_code": "RESOURCE_NOT_FOUND",
"request_id": "a1b2c3d4-..."
}
```
`404` also covers resources that exist but are outside your network
scope — the API does not distinguish "doesn't exist" from "not visible
to you".
Creating something that already exists, or modifying state that has moved
underneath you:
```json theme={null}
{
"detail": "Resource conflict",
"error_code": "RESOURCE_CONFLICT",
"request_id": "a1b2c3d4-..."
}
```
Re-read the current state before deciding whether to retry — a `409` on a
create often means the resource is already there and your work is done.
The `detail` is a single string in the form `: `,
where the path walks from the request part (`body`, `query`, `path`) down
to the offending field. Only the **first** validation error is reported.
```json theme={null}
{ "detail": "body -> first_name: Field required" }
```
```json theme={null}
{ "detail": "body -> date_of_birth: Input should be a valid date" }
```
```json theme={null}
{ "detail": "query -> network_id: Field required" }
```
Fix the named field and resend. The
[API Reference](/api-reference/introduction) documents the expected type
and format of every field.
Unexpected failures never leak internals:
```json theme={null}
{
"detail": "An unexpected error occurred",
"error_code": "INTERNAL_ERROR",
"request_id": "a1b2c3d4-..."
}
```
Known-but-failed internal operations return `OPERATION_FAILED` instead.
Both are logged and monitored on SOLO's side; include the `request_id`
when reporting one.
## Validation errors vs domain errors
The API draws a sharp line between two kinds of "bad request", and the status
code tells you which side you're on:
* **`422` — the request itself is malformed.** A required field is missing, a
date doesn't parse, a UUID is garbled. The request never reached business
logic. This is a bug in the calling code: fix the payload. The body is the
short shape with a `field path: message` detail.
* **`400` — the request is well-formed but the *operation* is invalid.** Every
field parsed, but a business rule said no: consent wasn't gathered, an update
contained nothing updatable, a required identifier was absent given the
combination of inputs. The body is the full envelope with
`"error_code": "VALIDATION_ERROR"`. Fixing this usually means changing what
you're asking for, not how you're serializing it.
Treat `422` as a build-time problem (your integration is constructing requests
wrong) and `400` as a run-time problem (this particular operation isn't
allowed right now, or needs different inputs).
## Request IDs
Every request is assigned a `request_id` as it enters the API, and error
envelopes include it whenever it's available. The same ID is attached to SOLO's
internal logs and traces for that request.
When contacting support about a failed call, always include:
1. The `request_id` from the error body (or the `X-Ref-Id` header for `204`
product-query responses).
2. The full response body and status code.
3. The endpoint, method, and approximate timestamp (with timezone).
With a `request_id`, support can jump directly to the exact request; without
one, they're searching by time window.
## Retry guidance
| Status | Retry? | How |
| -------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------- |
| `500` | Yes | Retry with exponential backoff and jitter. If it persists, stop and report the `request_id`. |
| `401` | Once | Refresh your token first, then retry a single time. Never loop with the same token. |
| `409` | Maybe | Re-read current state first; retry only if the conflict has been resolved. |
| `400`, `403`, `404`, `422` | No | The same request will fail the same way. Fix the request, your permissions, or the referenced resource. |
| `204` | No | Not an error — the query succeeded with no result. Re-query only when you expect the underlying data to have changed. |
Product query endpoints are billable. Build retry logic carefully so a
transient failure doesn't turn into a loop of repeated billable queries —
cap attempts, and use the non-billable `POST /v1/products/check` coverage
check when you only need to know whether data exists.
# High-level concepts
Source: https://docs.solo.one/home/overview
The six building blocks of the SOLO Network, the data flow that connects them, and how together they enforce privacy
The SOLO Network turns one institution's completed verification work into
another institution's trusted starting point. Participants furnish verified data
about consumers and businesses, and query that data back — under clear rules
about who can read what, and why. Six concepts power everything you do with the
API. Once you understand them and the order they come into play, every endpoint
in the [reference](/api-reference/introduction) reads naturally.
The architecture is not internal plumbing you can ignore — it *is* the trust
mechanism. Each building block exists to answer a question a bank's risk and
compliance teams must be able to answer before they reuse another institution's
work:
| Building block | Trust question it answers |
| -------------- | ------------------------------------------------------------------ |
| Network | Inside which boundary of trusted participants does this data live? |
| Entity | Who is the subject being verified? |
| Consent | Did the subject permit this read? |
| Product | What standardized, reusable result am I asking for? |
| Policy | What may be read, and how fresh must it be, to count? |
| Entitlement | Of what the policy exposes, what may *I* see — and why? |
Read [How SOLO is different](/home/why-solo) for why this matters, and
[Trust assets](/concepts/trust/trust-assets) for how a furnished verification
becomes something reusable.
## The building blocks
### Network
A [network](/concepts/governance/networks) is the trust boundary everything
else lives inside. It groups a set of participating institutions, defines which
products are available to them, and contains the data they furnish. Nothing
crosses a network boundary: a record furnished into one network is invisible to
every other, and every API call that touches data — a consent lookup, an entity
search, a product query — is scoped by a `network_id`. Within a network, each
participant holds one or more [roles](/concepts/governance/network-roles) —
governor, furnisher, querier — that determine which operations it may perform,
and [governance rules](/concepts/governance/network-governance) determine how
the network itself is administered and changed.
### Entity
An [entity](/concepts/identity/entities) is a subject of the network: a
**consumer** (a person) or a **business**. Every furnish writes data about an
entity, and every query reads data about one. Entities have a core identity —
name, email, SSN or EIN, date of birth — that the network uses to recognize
when two participants are talking about the same subject, so that data
furnished by one bank can be consolidated with data furnished by another. You
can look up existing entities with `GET /v1/entities/consumer/search` and
`GET /v1/entities/business/search`, both scoped by a required `network_id`.
### Consent
[Consent](/concepts/identity/consent) is the subject's permission, captured as
a first-class record. Before you query data about a consumer or business, you
record that they agreed — `POST /v1/consent/consumer` or
`POST /v1/consent/business` — and receive a `consent_id` that you pass with
every subsequent query. A consent record carries a scope, an optional expiry,
and the set of consented fields; scope, expiry, and consented fields can be
updated later, but the identity fields are immutable, because the record is an
audit artifact of who consented and when. The API refuses to create a consumer
consent unless you affirm that consent was actually gathered from the consumer
first.
### Product
A [product](/api-overview/querying/products) is the unit of data you furnish or
query — a standardized package rather than a raw table. The network currently
offers KYC and KYB certificates (consolidated identity-verification outcomes
for consumers and businesses respectively) and screening lists (a bank-specific
bad-actor list and a cross-bank financial-crimes watch list). Products expose
`query` and, where applicable, `furnish` operations under
`/v1/products/{product}/…`. A non-billable soft check,
`POST /v1/products/check`, reports per-furnisher field
[coverage](/api-overview/querying/coverage-check) for a product before you commit
to a billable query.
### Policy
A policy is a per-network, per-product rulebook authored by the network
governor. A [querying policy](/concepts/governance/querying-policies) defines
what a product is allowed to read — which models and which fields appear in
query results for that network. A
[furnishing policy](/concepts/governance/furnishing-policies) defines what a
product accepts on write and how submitted data is validated and transformed.
Policies are created as shells (`POST /v1/networks/policies/querying`,
`POST /v1/networks/policies/furnishing`) and then configured field by field via
their `PUT /v1/networks/policies/…/{policy_id}/configuration` endpoints. A
query names the policy it runs under, so the same product can behave
differently in different networks.
### Entitlement
[Entitlement](/concepts/governance/entitlement) answers the last question:
of the fields the policy exposes, which ones can *you* see? Entitlement is
earned through participation — by previously furnishing data about an entity or
querying it — and is evaluated per field at query time. Two participants
issuing the identical query against the same entity can receive different
results, each limited to the data their own history entitles them to. This is
the mechanism that makes a shared network safe: contributing participants are
never exposing data to passive observers.
## The data flow
The building blocks come into play in a fixed order. Here is the lifecycle of a
participant from joining a network to reading its first query result:
```mermaid theme={null}
sequenceDiagram
participant P as Participant
participant API as SOLO API (/v1)
participant N as Network
Note over P,N: 1. Join — governor grants roles
P->>API: GET /v1/entities/consumer/search?network_id=…
API->>N: scope to network, check role
N-->>P: matching entities
Note over P,N: 2. Consent — subject grants permission
P->>API: POST /v1/consent/consumer
API-->>P: consent_id
Note over P,N: 3. Furnish — contribute verified data
P->>API: POST /v1/products/kyc_certificate/furnish
API->>N: validate against furnishing policy, store
Note over P,N: 4. Query — read consolidated data
P->>API: POST /v1/products/kyc_certificate/query (consent_id)
API->>N: resolve policy, consent, entitlement
N-->>P: consolidated certificate (or 204 if no result)
```
1. **Join a network.** Your organization is added by the network governor and
assigned roles. From this point your token resolves to an account with
capabilities inside that network. See
[Join a network](/home/quickstart/join-a-network).
2. **Create consent.** Record the subject's permission and hold on to the
returned `consent_id`. Without it, product queries fail.
3. **Furnish.** Contribute data through a product's `furnish` endpoint — or in
bulk via [file upload](/api-overview/furnishing/file-upload) or
[SFTP](/api-overview/sftp/overview). The network's furnishing policy validates
what you send.
4. **Query.** Ask a product for consolidated data, citing your `consent_id` and
the network policy to run under. The result is assembled from every
furnisher's contributions — filtered through the policy and your
entitlement. If the available data cannot satisfy the policy's requirements,
you receive an empty `204 No Content` rather than a partial answer.
## How the pieces enforce privacy
The model is easiest to remember as three independent gates, each answering a
different question. All three must open before a field reaches your response.
| Gate | Question it answers | Granted by |
| ---------- | -------------------------------------- | -------------------------------------------------------------------------------- |
| Capability | May you perform this operation at all? | Your [roles](/concepts/governance/network-roles) in the network |
| Permission | Did the subject agree to this read? | A valid [consent](/concepts/identity/consent) record |
| Visibility | Which fields may you actually see? | Your [entitlement](/concepts/governance/entitlement), within the policy's bounds |
**Roles grant capability.** A participant without the querier role cannot query
anything, no matter how much consent it holds. Roles are assigned by the
network governor and checked on every request.
**Consent grants permission.** A querier with full capability still cannot read
a specific consumer's data without that consumer's consent record — scoped,
optionally time-limited, and auditable after the fact.
**Entitlement grants visibility.** Even with capability and permission, you see
only the fields your participation has earned, and never more than the
network's querying policy exposes to anyone. The policy is the ceiling;
entitlement is your position under it.
Because the gates are independent, no single actor can bypass the system: the
governor controls policies but not consent, the subject controls consent but
not entitlement, and your own history determines entitlement but not
capability.
## Where to go next
If you learn best by doing, the [Quickstart](/home/quickstart) chains the full
flow — consent, furnish, query — into a few `curl` commands. If you are about
to write integration code, read [Authentication](/home/authentication) to set
up your bearer token and [Errors](/home/errors) to handle failures correctly —
in particular the distinction between a `204` empty result, a `400` domain
rejection, and a `422` malformed request, all of which you will encounter while
integrating.
For depth on any single building block, follow its link above into the
concepts section. Two reading paths are worth calling out: if your organization
primarily contributes data, continue from
[Furnishing](/api-overview/furnishing/overview) into
[file upload](/api-overview/furnishing/file-upload) and the
[SFTP guide](/api-overview/sftp/getting-started) for bulk ingestion; if it
primarily consumes data, continue from
[Querying](/api-overview/querying/overview) into the individual product pages —
[KYC certificate](/api-overview/querying/kyc-certificate),
[KYB certificate](/api-overview/querying/kyb-certificate), and
[screening lists](/api-overview/querying/screening-lists).
## In the dashboard
How a query resolves across network, policy, consent, and entitlement.
How contributed data is validated, stored, and made queryable.
# Quickstart
Source: https://docs.solo.one/home/quickstart
Get up and running on the SOLO Network in a few minutes
The SOLO Network lets member banks contribute verified data about consumers and
businesses, and read consolidated views of that data back — all governed by
network membership, [consent](/concepts/identity/consent), and
[entitlement](/concepts/governance/entitlement). This quickstart sequences the
three workflows you'll use most, each as a self-contained walkthrough with
complete requests and responses you can adapt directly.
## The sequence
Work through the guides in order the first time:
1. **[Join a network](/home/quickstart/join-a-network)** — membership is arranged
with your SOLO account manager; this guide shows you how to verify that your
token, role, and `network_id` actually work before you build anything.
2. **[Furnish an entity](/home/quickstart/furnish-an-entity)** — record consent,
then contribute KYC certificate data about a consumer into your network.
3. **[Query a product](/home/quickstart/query-a-product)** — reuse the consent
and read a consolidated KYC certificate back, including how to interpret a
`200` versus a `204` and the `X-Ref-Id` header.
Furnishing and querying are independent capabilities, but they reinforce each
other: furnishing data about a consumer earns you
[entitlement](/concepts/governance/entitlement) to read that consumer's data on
future queries.
By the end of the sequence you'll have made every call a production
integration makes: a membership sanity check, a consent record, a furnish
submission, and a billable product query — and you'll know how to read each
response, including the empty ones.
```mermaid theme={null}
flowchart LR
Join[Join a network] --> Consent[Record consent]
Consent --> Furnish[Furnish an entity]
Consent --> Query[Query a product]
Furnish -.earns entitlement.-> Query
```
## What you'll need
| Requirement | Where it comes from | Used for |
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ |
| **Network membership + role** | Arranged with your SOLO account manager | Furnisher role to furnish, querier role to query |
| **Bearer token (SDK token)** | Issued by your SOLO account manager | The `Authorization` header on every request |
| **`network_id`** | Provided when your organization joins a network | Scoping every consent, search, furnish, and query |
| **Subnetwork name** (furnishers only) | Your network configuration | Routing furnished data to the right [furnishing policy](/api-overview/furnishing/overview) |
Export your token once so every example in these guides works as-is:
```bash theme={null}
export SOLO_TOKEN="your-sdk-token-here"
```
All endpoints live under `https://api.solo.one/v1` and authenticate with the
token as a Bearer credential:
```bash theme={null}
curl https://api.solo.one/v1/entities/consumer/search \
-H "Authorization: Bearer $SOLO_TOKEN"
```
See [Authentication](/home/authentication) for token claims and lifetimes, and
[Errors](/home/errors) for the error envelope every endpoint shares.
The walkthroughs use the consumer-side endpoints (`/v1/consent/consumer`,
`/v1/entities/consumer/search`, `/v1/products/kyc_certificate/...`). The
business-side equivalents — `POST /v1/consent/business`,
`GET /v1/entities/business/search`, and the KYB certificate under
`/v1/products/kyb_certificate` — follow the same patterns, so everything you
learn here transfers directly.
Every error response uses the same shape: a human-readable `detail` plus a
machine-readable `error_code` (for example `AUTHENTICATION_REQUIRED` or
`RESOURCE_NOT_FOUND`). Each walkthrough includes a troubleshooting section
with the exact failures you're likely to hit.
## Choose your path
Verify your token, membership, and role with your first two API calls.
Record consent, then contribute KYC data about a consumer.
Read a consolidated KYC certificate back from the network.
New to the platform's vocabulary? Read the
[high-level concepts](/home/overview) first — entities, products, policies,
networks, consent, and entitlement in a few minutes. For the full endpoint
catalog, see the [API reference](/api-reference/introduction).
# Golden path: business lending onboarding
Source: https://docs.solo.one/home/quickstart/business-lending-onboarding
Onboard a business by reusing verified trust assets first, then contributing only the work you had to do yourself
This golden path shows the **reuse-first** pattern for onboarding a business:
identify the subject, check what the network already holds, reuse it, do only
the missing work, and contribute your verification back. It chains real
endpoints end to end and uses the business / KYB side throughout.
```mermaid theme={null}
flowchart LR
Find["Identify business"] --> Consent["Record consent"]
Consent --> Check["Coverage check"]
Check --> Query["Query KYB certificate"]
Query -->|"200 reuse"| Decide["Underwrite"]
Query -->|"204 / gaps"| Furnish["Furnish missing verification"]
Furnish --> Requery["Re-query"]
Requery --> Decide
Decide --> Publish["Contribution stays in the network"]
```
## Prerequisites
* Network membership with the **querier** role (and **furnisher** role for
steps 5–6), arranged with your SOLO account manager.
* A bearer token exported as `SOLO_TOKEN` (see
[Authentication](/home/authentication)).
* Your `network_id`, and the KYB `product_id` for the coverage check.
Resolve the applicant to a network profile before anything else, so reused
and contributed work consolidate on one subject.
```bash theme={null}
curl -G https://api.solo.one/v1/entities/business/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…" \
--data-urlencode "business_legal_name=Acme Coffee" \
--data-urlencode "federal_ein=123456789"
```
Keep the `id` from any confident match to link it on consent. No match is
fine — consent will create the profile.
Capture permission to query the business and get a `consent_id`.
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/business \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"did_gather_consent_from_business_prior": true,
"business_consent_identity": {
"business_legal_name": "Acme Coffee LLC",
"business_jurisdiction_of_formation": "DE",
"business_registration_identifier_from_jurisdiction_of_formation": 7423918,
"business_tax_identifier_type": "EIN",
"business_tax_identifier_value": "12-3456789"
}
}'
```
The response carries the `consent_id` you'll pass on the coverage check and
query. See [Consent](/concepts/identity/consent) for the full shape.
Run the non-billable [coverage check](/api-overview/querying/coverage-check) to
see whether reusable KYB [trust assets](/concepts/trust/trust-assets)
already exist — and which furnishers can satisfy them — before spending a
billable query.
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/check \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "b4a1d6e8-…",
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
Each entity in the response carries a `furnishing_entity_name` and a
`complete` flag. A furnisher with `complete: true` can satisfy the KYB
product for this business — reuse is available.
Read the consolidated certificate. This reuses identity, ownership & control,
and risk/compliance verification already furnished by participants, each
block carrying its source's `furnishing_entity_id` and `attestation_id`.
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyb_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"],
"policy_id": "5e7d2a14-…"
}'
```
A `200` returns a certificate you can underwrite against. A `204` means the
available data did not satisfy your policy — continue to step 5.
For whatever the certificate could not supply (a `204`, or a sub-product the
coverage check showed as incomplete), perform that verification yourself and
contribute it back:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyb_certificate/furnish \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{ "business_legal_name": "Acme Coffee LLC", "business_ein": "12-3456789" }
]
}'
```
Furnishing earns you [entitlement](/concepts/governance/entitlement) to read
this business back later, and your verification becomes a reusable trust
asset for the next participant — attributed to you.
Re-run the step 4 query to assemble the now-complete certificate, then use it
in your underwriting decision. The `query_event_id` / `X-Ref-Id` ties the
network's record of the read to your internal decision record.
## Why this order
Reuse-first inverts the traditional onboarding loop: instead of collecting
everything up front, you collect *only what the network doesn't already have*.
The coverage check is free, so it always precedes the billable query; furnishing
last means your one verification compounds for every future participant.
## Related
The reuse-first read model in depth.
The business trust asset, field by field.
Pre-flight what already exists.
What you reuse and what you create.
# Furnishing your first entity
Source: https://docs.solo.one/home/quickstart/furnish-an-entity
Contribute verified data about a consumer into a network
Furnishing contributes data about a
[consumer or business](/concepts/identity/entities) into a
[network](/concepts/governance/networks). In this guide you'll record a
consumer's [consent](/concepts/identity/consent), then furnish KYC certificate
data for them. Furnishing is also how you earn
[entitlement](/concepts/governance/entitlement) — once you've contributed data
about a consumer, you can read that consumer's data back on future queries.
## Prerequisites
Before you start, you need:
* **Network membership with the furnisher role** — arranged with your SOLO
account manager (see
[Joining a network](/home/quickstart/join-a-network)).
* **A bearer token** (SDK token) exported as `SOLO_TOKEN`. See
[Authentication](/home/authentication).
* **Your `network_id`**, plus the **subnetwork name** you furnish under (many
setups use a single default subnetwork — ask your account manager if unsure).
## Record consent and furnish
Create a consent record with the consumer's identity, attesting that you
gathered their permission before contributing their data:
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/consumer \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"did_gather_consent_from_consumer_prior": true,
"consumer_consent_identity": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15T00:00:00",
"personal_email": "jane.doe@example.com",
"phone_number": "5551234567",
"social_security_number": "123-45-6789"
}
}'
```
```json Response theme={null}
{
"consent_id": "5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30",
"field_access_grants": [
{
"furnishing_entity_id": null,
"field_definitions": [
"fraud_verification_event.confirmed_fraud_indicator",
"fraud_verification_event.fraud_attribute_label",
"fraud_verification_event.fraud_event_date",
"fraud_verification_event.fraud_loss_event_category"
],
"effective_from": "2026-06-09",
"effective_to": null
}
],
"created_at": "2026-06-09T22:04:11+00:00",
"events": "",
"scope": "",
"expires_at": null,
"consented_fields": null
}
```
All six identity fields are required. If you already know the consumer's
UUID (for example from an
[entity search](/home/quickstart/join-a-network)), pass it as the optional
`consumer_id` to link the consent to that existing profile.
`did_gather_consent_from_consumer_prior` is an attestation, not a flag.
Only set it to `true` if you have actually obtained the consumer's
permission — the API rejects the request with a `400` otherwise.
You can re-fetch the consent at any time — the response body is identical
to the create response:
```bash theme={null}
curl -G https://api.solo.one/v1/consent/consumer/5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30 \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21"
```
Each furnish record identifies one consumer by their core identity. All
four fields are required:
```json theme={null}
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
```
These identifiers tell the network *which* consumer the data belongs to —
the certificate data is associated with that identity. The network matches
each record to a consumer (creating one if no match exists) and stores the
data under your organization, attributed to you as the furnisher.
`records` is a list, so batch as many consumers as you like into one call:
```json theme={null}
{
"records": [
{ "first_name": "Jane", "last_name": "Doe", "date_of_birth": "1990-01-15", "social_security_number": "123-45-6789" },
{ "first_name": "John", "last_name": "Smith", "date_of_birth": "1987-09-02", "social_security_number": "987-65-4321" }
]
}
```
Call the product's furnish endpoint with your network scope, subnetwork, and
records:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/furnish \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"subnetwork_name": "default",
"application_date": "2026-06-09",
"records": [
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
}'
```
```json Response theme={null}
{
"success": true,
"submission_id": "1d4f7a92-c3e5-48b0-a6d1-2b9c8e0f5a47"
}
```
Three fields route the furnish — this is **subnetwork routing**:
* `network_id` — the network you're furnishing into.
* `subnetwork_name` — the subnetwork (partition) within that network that
separates your submissions from other furnishers'.
* `application_date` — the date the data applies to.
Together they resolve the right furnishing policy automatically; you never
pass a policy yourself. See
[How furnishing works](/api-overview/furnishing/overview) for the resolution
rules and [Furnishing policies](/concepts/governance/furnishing-policies)
for how governors configure them.
The furnished consumer now appears in network-scoped entity search:
```bash theme={null}
curl -G https://api.solo.one/v1/entities/consumer/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21" \
--data-urlencode "social_security_number=123-45-6789"
```
A `200` with a matching record confirms the furnish landed. You've also
earned [entitlement](/concepts/governance/entitlement) to this consumer's
data on future queries.
## Inspect the result
**Consent response** (step 1):
| Field | Meaning |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `consent_id` | The token you'll pass on every subsequent query of this consumer. Store it. |
| `field_access_grants` | The access grants attached to the consent. Each grant lists its `field_definitions` (as `table.column` strings), the `furnishing_entity_id` it's scoped to (`null` = not furnisher-specific), and its `effective_from` / `effective_to` window. |
| `created_at` | When the consent record was created. |
| `events` | The consent's event log reference (empty for a brand-new consent). |
| `scope` | The consent's scope label. Starts unset; updatable later via `PUT /v1/consent/consumer/{consent_id}`. |
| `expires_at` | When the consent lapses. `null` means no expiry has been set. |
| `consented_fields` | An optional comma-separated restriction of consented fields. `null` means no field-level restriction. |
**Furnish response** (step 3):
| Field | Meaning |
| --------------- | ------------------------------------------------------------------------------------------ |
| `success` | `true` — the records were accepted and a furnish event was recorded. |
| `submission_id` | The UUID of this submission. Reference it when reconciling with your SOLO account manager. |
Identity fields on a consent are immutable — a consent record is an audit
artifact of who consented and when. To consent with different identity data,
create a new consent record. `scope`, `expires_at`, and `consented_fields`
are the only updatable attributes.
## Why consent comes first
Furnishing contributes data *about* a person, and the consent record is the
audit artifact proving they agreed: who consented, when, and through which
identity. Creating it before you furnish keeps your contribution defensible,
and the same `consent_id` is what you'll pass when you later
[query this consumer back](/home/quickstart/query-a-product) — one consent
covers both sides of your participation while it remains valid. The full
model, including field access grants and expiry, is covered in
[Consent](/concepts/identity/consent).
## Furnishing at scale
A JSON furnish accepts multiple `records` per call, which covers most
integration patterns. For recurring volume:
* **SFTP ingestion** — drop CSV files on a schedule and let the platform ingest
them automatically. Start with
[SFTP getting started](/api-overview/sftp/getting-started).
* **Bulk file workflows** — formats, templates, and validation behavior are
covered in [File upload](/api-overview/furnishing/file-upload).
## Troubleshooting
```json theme={null}
{
"detail": "Authentication required",
"error_code": "AUTHENTICATION_REQUIRED"
}
```
Your bearer token is missing, malformed, or expired. Re-export `SOLO_TOKEN`
with a fresh token from your SOLO account manager. See
[Authentication](/home/authentication).
```json theme={null}
{
"detail": "Consent cannot be created without prior consumer consent gathering",
"error_code": "VALIDATION_ERROR"
}
```
You sent `did_gather_consent_from_consumer_prior: false`. The platform will
not create a consent record without your attestation that the consumer
agreed first. Gather consent, then retry with `true`.
```json theme={null}
{
"detail": "Consent record not found",
"error_code": "RESOURCE_NOT_FOUND"
}
```
Returned by `GET /v1/consent/consumer/{consent_id}` when the `network_id`
query parameter doesn't match the network the consent was created in.
Consents are network-scoped — look them up with the same `network_id` you
created them under.
```json theme={null}
{
"detail": "Permission denied",
"error_code": "PERMISSION_DENIED"
}
```
Your token is valid and the network exists, but your membership doesn't
carry the furnisher role. Roles are arranged with your SOLO account
manager — see [Network roles](/concepts/governance/network-roles).
```json theme={null}
{
"detail": "body -> records: Field required"
}
```
A required field is missing or the wrong type. The `detail` string names
the first failing field as `location -> field: message` — common culprits
are omitting `subnetwork_name` or `application_date`, or leaving an identity
field out of `consumer_consent_identity`.
## Next steps
Reuse the `consent_id` and read this consumer's data back.
Subnetwork routing and policy resolution in depth.
Automated, recurring bulk ingestion.
The permission layer behind every read.
# Joining a network
Source: https://docs.solo.one/home/quickstart/join-a-network
Understand membership, roles, and your first calls in a network
Everything you do on the platform happens inside a
[network](/concepts/governance/networks) — a trust boundary that defines who can
contribute data, who can read it, and under which policies. This guide gets you
from "we signed the paperwork" to "our token works and we can see the network,"
using two read-only API calls.
## Prerequisites
Before you start, you need:
* **Network membership and a role** — arranged with your SOLO account manager
(see step 1 below).
* **A bearer token** (SDK token) exported as `SOLO_TOKEN`. See
[Authentication](/home/authentication).
* **Your `network_id`** — the UUID of the network you were added to.
## Set up and verify
Network membership and roles are arranged with your SOLO account manager —
there is no self-service signup endpoint. When your organization is added,
you receive:
* a **`network_id`** — the network you'll furnish into or query from, and
* one or more **roles** that define what you can do in it.
| Role | What it lets you do |
| ------------- | ----------------------------------------------- |
| **Furnisher** | Contribute data into the network. |
| **Querier** | Run product queries scoped to the network. |
| **Governor** | Administer the network's policies and metadata. |
Roles grant *capabilities*, not *visibility*. Joining a network — even
governing one — does not let you read other members' data. Reading requires
[consent](/concepts/identity/consent) from the data subject and
[entitlement](/concepts/governance/entitlement) earned by participating.
Roles are covered in depth in
[Network roles](/concepts/governance/network-roles).
Before testing your credentials, confirm you can reach the API at all.
`/version` requires no special role and tells you which API versions the
deployment serves:
```bash theme={null}
curl https://api.solo.one/version \
-H "Authorization: Bearer $SOLO_TOKEN"
```
```json Response theme={null}
{
"api_versions": ["v1"],
"build": {
"commit": "9b8a7c6d",
"version": "1.42.0"
}
}
```
All the endpoints in these guides live under the `v1` prefix listed in
`api_versions`. The `build` block identifies the exact deployment — include
it when reporting unexpected behavior to SOLO. There's also a
`GET /health` probe that returns `{"status": "healthy"}` if the service is
up; point your uptime monitoring at it.
The fastest end-to-end check of your token, membership, and `network_id` is
an entity search scoped to your network:
```bash theme={null}
curl -G https://api.solo.one/v1/entities/consumer/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21" \
--data-urlencode "last_name=Doe" \
--data-urlencode "limit=10"
```
```json Response theme={null}
[
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"identifier": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-05-12T18:21:43+00:00",
"updated_at": "2026-05-12T18:21:43+00:00",
"first_name": "Jane",
"last_name": "Doe",
"personal_email": "jane.doe@example.com",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
```
A `200 OK` — even with an empty `[]` body — proves your token is valid and
scoped to the network. `network_id` is the only required parameter; the
rest narrow the search:
* `first_name`, `last_name`, `personal_email` — case-insensitive **partial**
matches.
* `social_security_number`, `date_of_birth` — **exact** matches
(`YYYY-MM-DD` for date of birth).
* `limit` — maximum results, 1–100, default 20.
## Inspect the result
Each element of the search response is a consumer core identity visible to you
within the network:
| Field | Meaning |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `id` | The consumer's UUID. Use it as `consumer_id` when linking a consent record to a known profile. |
| `identifier` | The consumer record's stable string identifier. |
| `created_at` / `updated_at` | When the consumer record was created and last modified. |
| `first_name`, `last_name`, `personal_email`, `date_of_birth`, `social_security_number` | The core identity fields the record was matched on. Any of these may be `null` if not on file. |
An empty array is a perfectly healthy response — it means no consumer in this
network matches your filters yet. After you complete
[Furnishing your first entity](/home/quickstart/furnish-an-entity), the consumer
you furnish will appear here.
Note what the search does *not* return: certificates, events, or any product
data. Search is an identity lookup. Reading actual data about a consumer
always goes through a product query with a consent record — that separation is
the core of the platform's privacy model.
## Subnetworks
As you scale, you'll encounter **subnetworks** — narrower trust boundaries
nested under a parent network. You name the subnetwork when furnishing, and it
participates in [furnishing policy](/api-overview/furnishing/overview)
resolution.
Subnetworks are explained in [Networks](/concepts/governance/networks), and the rules
for who may change what are covered in
[Network governance](/concepts/governance/network-governance).
## Troubleshooting
```json theme={null}
{
"detail": "Authentication required",
"error_code": "AUTHENTICATION_REQUIRED"
}
```
Your bearer token is missing, malformed, or expired. Tokens are issued by
your SOLO account manager — re-export `SOLO_TOKEN` with a fresh value and
confirm the header reads exactly `Authorization: Bearer $SOLO_TOKEN`. See
[Authentication](/home/authentication).
```json theme={null}
{
"detail": "query -> network_id: Field required"
}
```
`network_id` is required on every search. A `422` always carries a single
`detail` string in `location -> field: message` form pointing at the first
invalid input — fix that field and retry.
```json theme={null}
{
"detail": "Permission denied",
"error_code": "PERMISSION_DENIED"
}
```
Your token is valid but your role in this network doesn't allow the call —
for example, furnishing without the furnisher role. Confirm your roles with
your SOLO account manager. See
[Network roles](/concepts/governance/network-roles).
Not an error. Your token and membership are fine; the network simply has no
consumer matching your filters. Partial-match fields (`first_name`,
`last_name`, `personal_email`) are the most forgiving way to probe for
data.
## Next steps
With membership verified, pick the workflow that matches your role:
## In the dashboard
For furnishers — record consent and contribute KYC data.
For queriers — read a consolidated certificate back.
Roles, subnetworks, and policy usage in depth.
What furnishers, queriers, and governors can each do.
# Golden path: consuming network data
Source: https://docs.solo.one/home/quickstart/network-consumption
The reuse-first decision pattern — check what verified work exists, reuse it, and do only what is missing
This golden path is the general **reuse-first** pattern for any consumer
onboarding or re-verification: before you collect and verify from scratch, ask
the network what reusable [trust assets](/concepts/trust/trust-assets) already
exist, reuse them, and limit your own work to the gaps. It uses the consumer /
KYC side; the business / KYB equivalent is
[Business lending onboarding](/home/quickstart/business-lending-onboarding).
```mermaid theme={null}
flowchart TB
Find["Identify subject"] --> Consent["Record consent"]
Consent --> Check["Coverage check: what exists?"]
Check --> Exists{Reusable assets exist?}
Exists -- yes --> Query["Query certificate · reuse"]
Exists -- no --> Collect["Collect + verify yourself"]
Query --> Partial{Complete for your policy?}
Partial -- yes --> Done["Use in decisioning"]
Partial -- no --> Collect
Collect --> Furnish["Furnish your verification"]
Furnish --> Done
```
## Prerequisites
* Network membership with the **querier** role (and **furnisher** role to
contribute), arranged with your SOLO account manager.
* A bearer token exported as `SOLO_TOKEN`.
* Your `network_id`, and the product's `product_id` for the coverage check.
Find an existing profile so reuse and any later contribution consolidate on
one consumer.
```bash theme={null}
curl -G https://api.solo.one/v1/entities/consumer/search \
-H "Authorization: Bearer $SOLO_TOKEN" \
--data-urlencode "network_id=9f1c0c2e-…" \
--data-urlencode "social_security_number=123-45-6789"
```
Capture the subject's permission and keep the `consent_id`.
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/consumer \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"did_gather_consent_from_consumer_prior": true,
"consumer_consent_identity": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"personal_email": "jane@example.com",
"phone_number": "+14155550100",
"social_security_number": "123-45-6789"
}
}'
```
The [coverage check](/api-overview/querying/coverage-check) is non-billable and
answers the reuse question directly: *which furnishers already hold complete
data for this subject and product?*
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/check \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "b4a1d6e8-…",
"consent_id": "5f3a8c21-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
If any furnisher is `complete: true`, reusable trust assets exist — query
them in the next step. If none are, skip to step 5 and do the work yourself.
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "5f3a8c21-…",
"network_ids": ["9f1c0c2e-…"],
"policy_id": "5e7d2a14-…"
}'
```
* **`200`** — reuse the consolidated certificate. Each populated sub-product
carries its source's `furnishing_entity_id` and `attestation_id`, so the
reuse is fully attributed.
* **`204`** — nothing you're entitled to read satisfied the policy. Treat it
as "do the missing work," not as an error.
Verify whatever the network couldn't supply, then contribute it back so the
next participant reuses it — and so you earn
[entitlement](/concepts/governance/entitlement) to read this subject later.
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/furnish \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-…",
"subnetwork_name": "default",
"application_date": "2026-05-28",
"records": [
{
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15",
"social_security_number": "123-45-6789"
}
]
}'
```
## The decision rule
Every step exists to push work onto the network before it falls on you:
1. **Identify** so contributions consolidate, not fragment.
2. **Check** (free) before you **query** (billable).
3. **Reuse** on `200`; **complete only the gaps** on `204` or partial coverage.
4. **Furnish** what you verified, turning a one-off cost into a reusable asset.
## Related
## In the dashboard
The same pattern on the business / KYB side.
Request anatomy, 200 vs 204, and the control plane.
The non-billable reuse pre-flight.
What you're reusing and creating.
# Querying your first product
Source: https://docs.solo.one/home/quickstart/query-a-product
Record consent and read consolidated data back from the network
Querying reads consolidated data about an entity from one or more
[networks](/concepts/governance/networks). Because you're reading personal
data, every query rides on a [consent](/concepts/identity/consent) record: you
create one (or reuse one), then pass its `consent_id` to the product's query
endpoint. In this guide you'll query a consolidated KYC certificate for a
consumer.
## Prerequisites
Before you start, you need:
* **Network membership with the querier role** — arranged with your SOLO
account manager (see
[Joining a network](/home/quickstart/join-a-network)).
* **A bearer token** (SDK token) exported as `SOLO_TOKEN`. See
[Authentication](/home/authentication).
* **Your `network_id`**. Optionally, the `policy_id` of a
[querying policy](/concepts/governance/querying-policies) — if you omit it,
each network's default policy applies.
## Consent, check, query
If you already hold a valid `consent_id` for this consumer — for example
from [Furnishing your first entity](/home/quickstart/furnish-an-entity) —
reuse it and skip to the next step. Otherwise, create one:
```bash theme={null}
curl -X POST https://api.solo.one/v1/consent/consumer \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"did_gather_consent_from_consumer_prior": true,
"consumer_consent_identity": {
"first_name": "Jane",
"last_name": "Doe",
"date_of_birth": "1990-01-15T00:00:00",
"personal_email": "jane.doe@example.com",
"phone_number": "5551234567",
"social_security_number": "123-45-6789"
}
}'
```
```json Response theme={null}
{
"consent_id": "5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30",
"field_access_grants": [
{
"furnishing_entity_id": null,
"field_definitions": [
"fraud_verification_event.confirmed_fraud_indicator",
"fraud_verification_event.fraud_attribute_label",
"fraud_verification_event.fraud_event_date",
"fraud_verification_event.fraud_loss_event_category"
],
"effective_from": "2026-06-09",
"effective_to": null
}
],
"created_at": "2026-06-09T22:04:11+00:00",
"events": "",
"scope": "",
"expires_at": null,
"consented_fields": null
}
```
Keep the `consent_id` — it identifies the consumer on every query while
the consent remains valid. You can confirm it later with
`GET /v1/consent/consumer/{consent_id}?network_id=...`.
`did_gather_consent_from_consumer_prior` is an attestation. Only send
`true` if you have actually obtained the consumer's permission; the API
rejects `false` with a `400`.
A query is a billable event. If you want to know whether the network holds
enough data *before* you commit, run the non-billable coverage soft check.
It reports per-furnisher field coverage for a product without producing a
certificate:
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/check \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "b4a1d6e8-2c7f-4e93-8a05-d9c3f1b7e642",
"consent_id": "5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30",
"network_ids": ["9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21"]
}'
```
```json Response theme={null}
{
"product_id": "b4a1d6e8-2c7f-4e93-8a05-d9c3f1b7e642",
"consumer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"business_id": null,
"entities": [
{
"furnishing_entity_id": "3e2d1c0b-9a87-4654-b321-0f9e8d7c6b5a",
"furnishing_entity_name": "First Example Bank",
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"complete": true,
"models": [
{
"model_name": "DocumentCaptureEvent",
"met_count": 4,
"total_count": 4,
"fields": [
{ "field_name": "is_document_captured", "met": true },
{ "field_name": "document_capture_timestamp", "met": true },
{ "field_name": "is_document_attribute_reviewed", "met": true },
{ "field_name": "document_attribute_review_timestamp", "met": true }
]
}
]
}
]
}
```
Any furnisher with `complete: true` can satisfy the product's data
requirements for this consumer. See
[Coverage check](/api-overview/querying/coverage-check) for the full
semantics.
Pass the `consent_id` and your network scope to the product's query
endpoint. `network_ids` takes one or more networks; the single optional
`policy_id` is applied across every network in the list, pairing each
network with that policy (omit it to use each network's default policy):
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30",
"network_ids": ["9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21"],
"policy_id": "5e7d2a14-8b3c-4f60-9e17-a2d4c6b8f015"
}'
```
```json Response theme={null}
{
"certificate_id": "8a6f4c2e-1b9d-4730-a5e8-3c7f0d2b6491",
"query_event_id": "c1e8f5a3-7d20-4b96-8c44-e6a9b3d1f728",
"consumer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"result": {
"meta": {
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"policy_id": "5e7d2a14-8b3c-4f60-9e17-a2d4c6b8f015"
},
"document_capture": {
"furnishing_entity_id": "3e2d1c0b-9a87-4654-b321-0f9e8d7c6b5a",
"attestation_id": "f0a9b8c7-d6e5-4432-a1b0-c9d8e7f6a5b4",
"assertions": {
"document_capture_assertion": true,
"document_capture_timestamp": "2026-04-15T00:00:00"
},
"data": {
"document_type": "passport",
"document_number": "987654321",
"document_issue_date": "2020-08-05",
"document_expiration_date": "2030-08-05",
"document_issuing_state": "government",
"document_capture_method": "mobile_scan"
}
},
"document_review": {
"furnishing_entity_id": "3e2d1c0b-9a87-4654-b321-0f9e8d7c6b5a",
"attestation_id": "f0a9b8c7-d6e5-4432-a1b0-c9d8e7f6a5b4",
"assertions": {
"document_attribute_review_assertion": true,
"document_attribute_review_timestamp": "2026-04-16T00:00:00"
},
"data": {
"document_review_method": "automated_ocr",
"document_tamper_review_performed": true,
"document_tamper_indicators_observed": false
}
},
"biometric_capture": {
"furnishing_entity_id": "3e2d1c0b-9a87-4654-b321-0f9e8d7c6b5a",
"attestation_id": "a7b6c5d4-e3f2-4109-b8a7-c6d5e4f3a2b1",
"assertions": {
"biometric_capture_assertion": true,
"biometric_capture_timestamp": "2026-04-15T00:00:00"
},
"data": {
"biometric_capture_method": "selfie"
}
},
"biometric_review": null,
"liveness_capture": null,
"liveness_review": null,
"address_capture": null,
"address_verification": null,
"identity_corroboration": null
}
}
```
To restrict the query to specific furnishers, add the optional
`furnishing_entity_ids` list; when omitted, data from all furnishers is
considered.
## 200 vs 204, and the X-Ref-Id header
A query has two healthy outcomes:
* **`200 OK`** — the available data satisfied the policy's requirements and a
certificate was created. The body is the consolidated result above.
* **`204 No Content`** — the query ran, but no certificate could be created:
the data available to you didn't satisfy the policy requirements. The body
is empty.
Both outcomes carry an **`X-Ref-Id` response header** — the UUID of the
recorded query event. On a `200` it equals the body's `query_event_id`; on a
`204` it's the only identifier you get, so log it. Quote the `X-Ref-Id` when
raising an issue with SOLO, and use it to reconcile query activity.
A `204` is not an error and not necessarily "the network has nothing." It
means the fields *you* can read — bounded by
[entitlement](/concepts/governance/entitlement) and the querying
[policy](/concepts/governance/querying-policies) — were insufficient to
assemble a complete certificate. The coverage check in step 2 is the cheap
way to predict this.
## Inspect the result
| Field | Meaning |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `certificate_id` | UUID of the certificate created by this query. |
| `query_event_id` | UUID of the recorded query event; matches the `X-Ref-Id` header. |
| `consumer_id` | UUID of the consumer the consent resolved to. |
| `result.meta` | The anchor `network_id` the result is attributed to and the `policy_id` applied (`null` when the default policy was used). |
| `result.document_capture` … `result.identity_corroboration` | The nine certificate sub-products: document capture/review, biometric capture/review, liveness capture/review, address capture/verification, and identity corroboration. Each populated sub-product carries the `furnishing_entity_id` and `attestation_id` of its source, an `assertions` block (what was asserted and when), and a `data` block with the underlying detail fields. |
A sub-product is `null` when no furnisher's data satisfied it — which is
expected, not a failure, as long as the policy doesn't require it. If a field
you expected is missing, it's usually because you haven't earned entitlement
to it (you've neither furnished nor previously queried it) or the policy
doesn't expose it in this network.
## Maintaining the consent
Reuse one `consent_id` for every query of the same consumer while the consent
is valid. Its identity fields are immutable, but you can adjust `scope`,
`expires_at`, and `consented_fields` as your agreement with the consumer
evolves:
```bash theme={null}
curl -X PUT https://api.solo.one/v1/consent/consumer/5f3a8c21-09d4-4b7a-9d52-6e8b1f4c2a30 \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network_id": "9f1c0c2e-3d5b-4a89-b1e2-7f6a8c0d4e21",
"expires_at": "2027-01-01T00:00:00+00:00"
}'
```
The response is the updated consent in the same shape as the create response.
At least one of the three updatable fields must be present, and the
`network_id` must match the network the consent was created in. To consent
with different identity data, create a new record instead.
## Troubleshooting
```json theme={null}
{
"detail": "Authentication required",
"error_code": "AUTHENTICATION_REQUIRED"
}
```
Your bearer token is missing, malformed, or expired. Re-export `SOLO_TOKEN`
with a fresh token from your SOLO account manager. See
[Authentication](/home/authentication).
```json theme={null}
{
"detail": "Consent cannot be created without prior consumer consent gathering",
"error_code": "VALIDATION_ERROR"
}
```
Returned by the consent create in step 1 when
`did_gather_consent_from_consumer_prior` is `false`. Gather the consumer's
permission first, then retry with `true`.
```json theme={null}
{
"detail": "Consent record not found",
"error_code": "RESOURCE_NOT_FOUND"
}
```
Two common causes: you looked up a consent with
`GET /v1/consent/consumer/{consent_id}` using a `network_id` other than
the one it was created in (consents are network-scoped), or your query's
`consent_id` doesn't resolve to a known consumer. Verify the `consent_id`
and use the originating `network_id`.
```json theme={null}
{
"detail": "body -> network_ids: Field required"
}
```
`network_ids` (with at least one entry) is required on every query, and
`consent_id` identifies the subject. A `422` carries a single `detail`
string in `location -> field: message` form pointing at the first invalid
input.
Empty body, status `204`, with an `X-Ref-Id` header identifying the query
event. The data you're entitled to read didn't satisfy the policy. Run the
[coverage check](/api-overview/querying/coverage-check) to see which furnishers
and fields fall short, or revisit which networks and `policy_id` you're
querying with.
## Next steps
## In the dashboard
Network + policy + product + consumer, end to end.
The permission layer behind every query.
The non-billable soft check in depth.
KYB certificates, screening lists, and the rest of the catalog.
# Overview
Source: https://docs.solo.one/home/welcome
What the SOLO Network is, who participates in it, and where each kind of participant should start
SOLO is **not a data aggregator**. It is a reusable verification network.
A data provider answers *"what can we discover about this business or
consumer?"* — and every institution starts that discovery from scratch. SOLO
answers a different question: *"what has already been verified about this
subject, who verified it, on what evidence, and can I safely reuse it?"* When
one institution verifies a consumer or business, that completed work becomes a
**reusable, permissioned, attributable result** that other participants can
build on instead of repeating.
> Data providers help you *verify* a business. SOLO helps you avoid *verifying
> the same business again.*
Banks and other institutions **furnish** verified data about consumers and
businesses into networks they belong to, and **query** that data back — under
explicit, auditable rules about who can read what, and why. Instead of
integrating with dozens of verification vendors one at a time, you connect to
SOLO once and work with standardized **products** — a KYC certificate, a KYB
certificate, screening lists — inside a
[network](/concepts/governance/networks) of participants you already trust.
Every read is gated by the subject's [consent](/concepts/identity/consent),
shaped by the network's policies, and limited to the fields you are entitled to
see.
## Verify once, reuse everywhere
In the traditional model, every application triggers a fresh round of
verification. Three banks onboarding the same business each collect the same
documents, run the same registry lookups, and trace the same ownership — paying
again to rediscover the same facts. In a SOLO network, the first verification
is preserved as a reusable result; later participants reuse it, with full
provenance, and only do the work that is genuinely missing.
```mermaid theme={null}
flowchart TB
subgraph traditional [Traditional verification - repeat every time]
direction LR
A1[Application 1] --> V1["Verify identity, ownership, income"] --> D1[Decision]
A2[Application 2] --> V2["Verify identity, ownership, income"] --> D2[Decision]
A3[Application 3] --> V3["Verify identity, ownership, income"] --> D3[Decision]
end
subgraph solo [SOLO network - verify once and reuse]
direction LR
S1[Application 1] --> SV1["Verify once"] --> TA[("Reusable verified result")]
S2[Application 2] --> TA
S3[Application 3] --> TA
TA --> SD["Decision faster"]
end
```
See [How SOLO is different](/home/why-solo) for the full contrast with
traditional data and KYB/KYC providers.
## How the network works
Every interaction with the API is one of two operations on a product:
1. **Furnish** — contribute verified data about a
[consumer or business](/concepts/identity/entities) into a network. Other
participants can then query it, subject to the network's rules.
2. **Query** — read consolidated data about an entity from the network. A query
requires a `consent_id` proving the subject agreed, and returns only the
fields the network's policy exposes and you are
[entitled](/concepts/governance/entitlement) to read.
```mermaid theme={null}
flowchart LR
You[Your system] -->|furnish| N[(SOLO Network)]
You -->|query + consent| N
Others[Other participants] -->|furnish| N
N -->|consolidated result| You
```
The product catalog covers the core verification workload of a bank:
**KYC certificates** consolidate identity-verification outcomes for consumers,
**KYB certificates** do the same for businesses, and two **screening lists** —
a bank-specific bad-actor list and a cross-bank financial-crimes watch list —
answer targeted risk questions. Before committing to a billable query, you can
run a non-billable coverage check (`POST /v1/products/check`) to see whether
the network holds enough data about a subject to make the query worthwhile.
The rules sit in the network itself: roles define what each participant *can*
do, policies define what each product reads and writes, and entitlement defines
what each participant *sees*. [High-level concepts](/home/overview) walks
through the full model.
The API itself is plain REST: JSON request and response bodies, standard HTTP
status codes, and a single versioned prefix (`/v1`) for every public endpoint.
Errors follow one consistent envelope — see [Errors](/home/errors) — so the
handling code you write for your first endpoint works for all of them.
## Where to start
Most organizations play one or more of three roles in a network. Find yours and
follow its path.
### Network governors
You operate a network: you decide which products it offers, what each product
is allowed to read and write, and who participates. Start with
[Networks](/concepts/governance/networks) to understand the trust boundary you
are administering, then define the rules of the road with querying and
furnishing policies — `POST /v1/networks/policies/querying` and
`POST /v1/networks/policies/furnishing` create the policy shells, and the
`PUT .../{policy_id}/configuration` endpoints author the per-field selections.
The [Join a network](/home/quickstart/join-a-network) quickstart shows the
participant side of what you are governing.
### Furnishers
You contribute data: KYC or KYB outcomes your institution has already verified.
Start with the [Furnishing overview](/api-overview/furnishing/overview) to learn
how contributed data is validated and stored, then run the
[Furnish an entity](/home/quickstart/furnish-an-entity) quickstart to push your
first record through `POST /v1/products/kyc_certificate/furnish`. High-volume
furnishers can ingest files through `POST /v1/file-upload/ingest` or automated
SFTP delivery instead of per-record API calls.
### Queriers
You consume data: you want a consolidated KYC certificate or a screening-list
answer for an entity you are onboarding. Start with the
[Querying overview](/api-overview/querying/overview) to see how a query resolves
across network, policy, and consent, and read
[Consent](/concepts/identity/consent) — you must record consent and obtain a
`consent_id` before any query. Then run the
[Query a product](/home/quickstart/query-a-product) quickstart, which takes you
from `POST /v1/consent/consumer` to
`POST /v1/products/kyc_certificate/query` end to end.
Not sure which role you hold? Your network governor assigned it when your
organization joined. Many participants are both furnishers and queriers — the
two paths share the same concepts, so read both overviews.
## Environments
The API is served per environment. All public endpoints sit under the `/v1`
prefix and require a bearer token (see
[Authentication](/home/authentication)).
| Environment | Base URL | Purpose |
| ----------- | ------------------------------ | ------------------------------------- |
| Development | `https://api.dev.solo.one` | Early integration and experimentation |
| Sandbox | `https://api.sandbox.solo.one` | Pre-production testing with test data |
| Production | `https://api.prod.solo.one` | Live traffic |
Two endpoints are available without authentication for monitoring:
`GET /health` returns `{"status": "healthy"}`, and `GET /version` reports the
supported API versions and build information.
Credentials are scoped to a single environment. A token minted for sandbox
will not authenticate against production, and vice versa.
Build against sandbox first: it mirrors production behavior — including
consent enforcement, policy evaluation, and the full error model — without
touching live data, so your integration is exercised end to end before you
switch base URLs.
## Next steps
Why SOLO is a reusable trust network, not a data aggregator.
How a furnished verification becomes reusable, with provenance intact.
Furnish an entity, query a product, and join a network — hands-on, in
minutes.
Networks, entities, consent, products, policies, and entitlement — and how
they fit together.
Endpoint-by-endpoint request and response schemas for the full `/v1`
surface.
# How SOLO is different
Source: https://docs.solo.one/home/why-solo
Why SOLO is a reusable trust network, not a data aggregator — and how the architecture makes verified work reusable
Most data and KYB/KYC vendors share one shape: they aggregate from external
sources and hand each institution a fresh answer. SOLO has a different shape: it
**preserves completed verification work** and lets the next institution reuse
it, with provenance intact. This page makes that distinction concrete.
## Two architectures
A traditional provider sits between external sources and the institution. Every
institution that asks a question pays to rediscover the same facts.
```mermaid theme={null}
flowchart LR
SoS[Secretary of State] --> Vendor
IRS[IRS] --> Vendor
Watch[Watchlists] --> Vendor
Web[Web data] --> Vendor[Data provider]
Vendor --> Inst["Institution (starts from scratch every time)"]
```
SOLO sits *between institutions*. The institution that does the verification
creates a reusable result; the network preserves it, and other participants
reuse it under permission.
```mermaid theme={null}
flowchart LR
A["Institution A"] --> Verify["Verifies the subject"]
Verify --> Result[("Reusable verified result + provenance")]
Result --> Network[(SOLO Network)]
Network --> B["Institution B reuses it"]
Network --> C["Institution C reuses it"]
Verify -.retains attribution + control.-> A
```
This is not better data aggregation. It is **trust portability**: the
verification stops dying at the end of a single application.
## The one-sentence difference
The same idea, said a few ways:
* Data providers help you *verify* a business. SOLO helps you avoid *verifying
the same business again*.
* Providers *sell information*. SOLO makes *verified work reusable*.
* Aggregators answer *"what data exists?"* SOLO answers *"what trust already
exists?"*
## Side by side
| Traditional data / KYB / KYC providers | SOLO |
| -------------------------------------- | ----------------------------------------------- |
| Discover information | Reuse verified information |
| Aggregate external sources | Preserve completed verification work |
| Every institution verifies again | Participants reuse prior verification |
| Vendor owns the intelligence | Network participants create the intelligence |
| Point-in-time lookup | Persistent, queryable verification |
| More applications create more work | More applications create more reuse |
| You only pay for data | You contribute *and* benefit from verified work |
## Every claim above maps to a real mechanism
This is not aspirational positioning. Each contrast is backed by a feature that
exists in the API today:
| Claim | How SOLO actually does it |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Reuse verified information" | A [product](/api-overview/querying/products) query *consolidates* verification events furnished by participants into a single [KYC](/api-overview/querying/kyc-certificate) or [KYB](/api-overview/querying/kyb-certificate) certificate, instead of re-collecting the underlying evidence. |
| "Preserve completed verification work" | Furnished work is stored as attested verification events and re-resolved on every future query — see [Trust assets](/concepts/trust/trust-assets). |
| "Attributable" | Every certificate sub-product carries the `furnishing_entity_id` and `attestation_id` of its source — see [Provenance](/concepts/trust/provenance). |
| "Permissioned" | Reads require the subject's [consent](/concepts/identity/consent), and fields are filtered by your [entitlement](/concepts/governance/entitlement). |
| "How fresh must it be to count?" | [Querying policies](/concepts/governance/querying-policies) enforce freshness windows; data outside the window is not reused (a `204`, not a stale answer). |
| "Contribute *and* benefit" | Furnishing earns the [entitlement](/concepts/governance/entitlement) that lets you read an entity back later — contribution and consumption are the same loop. |
| "Know before you pay" | The non-billable [coverage check](/api-overview/querying/coverage-check) answers *"does reusable verified work already exist for this subject?"* before a billable query. |
## What SOLO is not
To avoid the wrong takeaway:
* **Not another KYB/KYC workflow tool.** SOLO does not replace your verification
vendors with one more; it makes the verifications already done reusable across
the network.
* **Not an aggregator with permissions bolted on.** The unit of value is a
*reusable, attributed verification*, not a fresh external lookup.
* **Not a black box you must trust blindly.** The architecture is exposed on
purpose — provenance, consent, entitlement, and policy are all visible,
because visibility is what makes participants comfortable reusing each other's
work.
## Where to go next
How a furnished verification becomes something reusable.
Who verified what, when, and on what evidence.
Networks, entities, consent, products, policies, entitlement.
# Bank-Specific Bad Actor List
Source: https://docs.solo.one/products/bank-specific-bad-actor-list
Sponsor-bank-scoped screening for repeat policy violators
The **Bank-Specific Bad Actor List** is a sponsor-bank-scoped list of specific
individuals who, as prior customers of one or more of the bank's fintech
partners, violated one or more documented bank policies. It answers a narrow
question — *is this consumer on this bank's bad actor list?* — and returns a
listing indicator plus a small set of context fields. It never issues a reusable
certificate; it is a point-in-time check.
| | |
| ------------- | ------------------------------------------------------------------------- |
| **Category** | Fraud & Financial Crime |
| **Use case** | Bad actor screening |
| **Subject** | Consumer |
| **Family** | List (query-only) |
| **Scope** | Your sponsor bank's network only — results never cross network boundaries |
| **Operation** | `POST /v1/products/bank_specific_bad_actor_list/query` |
Because it is scoped to one sponsor bank, a consumer flagged in Bank A's network
is invisible to Bank B. The events behind the list are contributed by network
participants through governed furnishing flows.
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/bank_specific_bad_actor_list/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": null,
"is_listed": false,
"bad_actor_list_placement_date": null,
"bad_actor_list_reason_code": null
}
```
A clean result is `"is_listed": false` (always `200 OK`, never `204`). When the
consumer is listed, `is_listed` is `true` and the context fields are populated
with a placement date and a reason code.
## Matching & fields
Subjects are matched on the consumer identity behind your
[consent](/concepts/identity/consent) record. SSN and date of birth are
mandatory match inputs; name, phone, and email refine the match.
Reason codes are drawn from a documented set — `account_abuse`, `fraud`, and
`policy_violation` — and each listing carries the bank's definition of the
violated policy in `bad_actor_reason_definition`.
| Field | API name | Type |
| ----------------------------- | ------------------------------- | ------ |
| Bad Actor Reason Code | `bad_actor_reason_code` | String |
| Bad Actor Reason Definition | `bad_actor_reason_definition` | String |
| Bad Actor List Placement Date | `bad_actor_list_placement_date` | Date |
The screening lists deep dive — match inputs, per-list event fields, reason
codes, and compliance posture.
## Related
The full product catalog.
The cross-bank counterpart, visible across the consortium.
# Confirmed Fraud Attribute List
Source: https://docs.solo.one/products/confirmed-fraud-attribute-list
Consortium screening for identifying attributes tied to confirmed fraud
The **Confirmed Fraud Attribute List** is a consortium for banks and fintechs to
furnish discrete personal identifying attributes tied to **confirmed** fraud
events. Rather than re-investigating a subject from scratch, a query checks
whether the consumer is associated with a confirmed fraud event and surfaces the
specific identifying attributes that were implicated. It never issues a reusable
certificate; it is a point-in-time check.
| | |
| ------------- | -------------------------------------------------------- |
| **Category** | Fraud & Financial Crime |
| **Use case** | Bad actor screening |
| **Subject** | Consumer |
| **Family** | List (query-only) |
| **Operation** | `POST /v1/products/confirmed_fraud_attribute_list/query` |
The distinction from the other lists is its focus on **attributes**: each
listing ties a confirmed fraud event to discrete identifying data (the
`fraud_attribute_label` and its content), so consumers of the list learn not
just *that* fraud occurred but *which* identity attributes were involved. The
events behind it are contributed by network participants through governed
furnishing flows.
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/confirmed_fraud_attribute_list/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
A clean result is `"is_listed": false` (always `200 OK`, never `204`). When the
consumer is tied to a confirmed fraud event, the attributes from the product's
`FraudVerificationEvent` schema are populated:
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": "e1d2c3b4-…",
"is_listed": true,
"confirmed_fraud_indicator": true,
"fraud_attribute_label": "phone_number",
"fraud_event_date": "2025-12-02",
"fraud_loss_event_category": "account-takeover",
"fraud_malicious_intent_method": "phishing"
}
```
## Matching & fields
Subjects are matched on the consumer identity behind your
[consent](/concepts/identity/consent) record. SSN and date of birth are
mandatory match inputs; name, phone, and email refine the match.
The fraud attributes are defined by the product's `FraudVerificationEvent`
schema. *Field* is the display name and *API name* is the `field_name` used in
policies.
| Field | API name | Type |
| --------------------------------------------------- | ----------------------------------------------------- | ------- |
| Confirmed Fraud Indicator | `confirmed_fraud_indicator` | Boolean |
| Fraud Attribute Label | `fraud_attribute_label` | String |
| Fraud Attribute ID | `fraud_attribute_id` | UUID |
| Fraud Attribute Content | `fraud_attribute_content` | String |
| Fraud Event ID | `fraud_event_id` | UUID |
| Fraud Event Date | `fraud_event_date` | Date |
| Fraud Loss Event Category | `fraud_loss_event_category` | String |
| Fraud Loss Event Documentation Upload | `fraud_loss_event_documentation_upload` | String |
| Fraud Malicious Intent Method | `fraud_malicious_intent_method` | String |
| Fraud Malicious Intent Lineage Documentation Upload | `fraud_malicious_intent_lineage_documentation_upload` | String |
Loss-event categories include `financial-theft`, `account-takeover`, and
`synthetic-identity`; malicious-intent methods include `phishing` and
`card-not-present`. A network's
[querying policy](/concepts/governance/querying-policies) can restrict which
categories and methods a query considers.
Confirmed-fraud signals are screening inputs, not adjudications. Whether a
listing can support adverse action depends on your program's compliance
framework. Route hits to your compliance review process.
## Related
The full product catalog.
Sponsor-bank-scoped repeat-offender screening.
# Coverage Check
Source: https://docs.solo.one/products/coverage-check
Non-billable pre-flight of field coverage before running a query
The **coverage check** is a non-billable, read-only pre-flight for product
queries. Given a product, a subject, and a network + policy scope, it reports —
**per furnisher** — which of the policy's required fields are covered by data
already furnished to the network. It answers *"if I run this query now, can it
succeed?"* without issuing a certificate, recording a query event, or creating a
billable event.
| | |
| ------------- | -------------------------------------------------- |
| **Category** | Utility (not a product query) |
| **Use case** | Pre-flight a query, avoid predictable `204`s |
| **Subject** | Consumer or business (matches the checked product) |
| **Family** | Non-billable check |
| **Operation** | `POST /v1/products/check` |
One endpoint serves every product: pass the `product_id` of the product you
intend to query.
## Why it exists
A product query that resolves but cannot satisfy the policy returns
[`204 No Content`](/api-overview/querying/overview#200-vs-204-here-is-data-vs-nothing-usable)
— and that resolved query is still a billable event. The coverage check lets you
avoid predictable `204`s for free.
```mermaid theme={null}
sequenceDiagram
participant App as Your app
participant API as SOLO API
App->>API: POST /v1/products/check (non-billable)
API-->>App: per-furnisher coverage
alt coverage complete
App->>API: POST /v1/products/{product}/query (billable)
API-->>App: 200 + certificate
else coverage incomplete
App->>App: skip query / adjust UX / pick another policy
end
```
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/check \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"product_id": "3e8a1d5f-…",
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
A `200 OK` returns one entry per furnisher that has relevant data, each with a
`complete` flag and a per-model, per-field breakdown of which requirements are
`met`. The `field_name` values trace directly to each product's field reference.
The coverage check is a *soft* check. It does not issue a certificate, return
subject data, record a query event, or guarantee the subsequent query returns
`200` — data can change between the check and the query.
The complete coverage check deep dive — full request fields, the response
shape, and when to use it.
## Related
The full product catalog.
The billable query the coverage check pre-flights.
# Cross-Bank Financial Crimes Watch List
Source: https://docs.solo.one/products/cross-bank-financial-crimes-watch-list
314(b) consortium screening for inter-bank financial-crimes signals
The **314(b) Cross-Bank Financial Crimes Watch List** is a cross-bank shared
consortium that extends beyond one sponsor-bank network and focuses on sharing
suspicious financial-crimes activity. It is governed by banks as defined by
31 CFR 1020.100(d), under 314(b) permissible purpose. It answers — *is this
consumer associated with suspicious financial-crimes activity reported across
participating banks?* — returning a listing indicator plus a small set of
context fields. It never issues a reusable certificate.
| | |
| ------------- | ---------------------------------------------------------------- |
| **Category** | Fraud & Financial Crime |
| **Use case** | Watch list screening |
| **Subject** | Consumer |
| **Family** | List (query-only) |
| **Scope** | All participating banks — visibility spans the consortium |
| **Operation** | `POST /v1/products/cross_bank_financial_crimes_watch_list/query` |
Unlike the [Bank-Specific Bad Actor List](/products/bank-specific-bad-actor-list),
this list is cross-bank by design: it exists so that suspicious financial-crimes
activity observed at one bank is visible to the others, under the 314(b)
information-sharing framework. The events behind it are contributed by network
participants through governed furnishing flows.
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/cross_bank_financial_crimes_watch_list/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
```json theme={null}
{
"query_event_id": "7b2d9c4e-…",
"consumer_id": "c2a4e8d0-…",
"furnishing_entity_id": null,
"is_listed": false,
"entity_level_adverse_action_eligible_indicator": null,
"watch_list_placement_date": null
}
```
A clean result is `"is_listed": false` (always `200 OK`, never `204`). When the
consumer is listed, the context fields — including a placement date and the
`entity_level_adverse_action_eligible_indicator` — are populated.
## Matching & fields
Subjects are matched on the consumer identity behind your
[consent](/concepts/identity/consent) record. SSN and date of birth are
mandatory match inputs; name, phone, and email refine the match.
Event categories include `money_laundering`, `terrorist_financing`, and `fraud`;
signal levels are `high`, `medium`, or `low`. A network's
[querying policy](/concepts/governance/querying-policies) can restrict which
categories and signal levels a query considers — for example, only `high`-signal
events.
| Field | API name | Type |
| -------------- | ---------------- | ------ |
| Event Date | `event_date` | Date |
| Event Category | `event_category` | String |
| Signal Level | `signal_level` | String |
Watch-list signals are screening inputs, not adjudications. Whether a listing
can support adverse action depends on your program's compliance framework —
this list operates under 314(b) permissible purpose, and the
`entity_level_adverse_action_eligible_indicator` exists precisely because not
every signal qualifies. Route hits to your compliance review process.
The screening lists deep dive — match inputs, per-list event fields, and
compliance posture.
## Related
The full product catalog.
The sponsor-bank-scoped counterpart.
# KYB Certificate
Source: https://docs.solo.one/products/kyb-certificate
Reusable business-verification certificate for business onboarding
The **KYB Certificate** is a reusable Know Your Business attestation that
packages UBO (ultimate beneficial owner), incorporation, and business-identity
evidence into a single certificate. Rather than each institution repeating
registry lookups, ownership tracing, and risk screening for the same business, a
query assembles the verification work already furnished by network participants
into one network-issued result.
| | |
| -------------- | -------------------------------------------------------------------------------------- |
| **Category** | Identity Verification |
| **Use case** | Customer onboarding |
| **Subject** | Business |
| **Family** | Certificate (issues a `certificate_id`) |
| **Operations** | `POST /v1/products/kyb_certificate/query`, `POST /v1/products/kyb_certificate/furnish` |
## What's in the certificate
A KYB certificate consolidates three **sub-products**, each a block in the query
response with its own `assertions` (what was attested, and when) and `data` (the
supporting attributes):
| Sub-product | Response key | What it attests |
| -------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Business identity verification | `business_identity_verification` | The legal entity exists and is what it claims — registration, jurisdiction, tax ID validation, operational existence. |
| Ownership & control verification | `business_ownership_control_verification` | Beneficial owners, control persons, and authorized representatives were identified and evidenced. |
| Risk & compliance assessment | `business_risk_compliance_assessment` | Sanctions, adverse media, restricted-activity, and activity-risk screening were performed. |
Every populated sub-product carries the `furnishing_entity_id` of the
participant whose data backed it and the `attestation_id` of their attestation.
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyb_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
A `200 OK` means a certificate was issued — the response carries a
`certificate_id`, the billable `query_event_id`, and a `result` block with each
sub-product (sub-products without qualifying data are `null`). A `204 No Content`
means the available data did not satisfy the policy.
The complete KYB certificate deep dive — every model, the full response
example, how it resolves per network, and the `204` rules.
## Related
The full product catalog.
Check field coverage before running a billable query.
# KYC Certificate
Source: https://docs.solo.one/products/kyc-certificate
Reusable consumer identity-verification certificate for onboarding
The **KYC Certificate** is a reusable identity-verification certificate for
consumer onboarding. Instead of each institution re-running document capture,
biometric checks, and identity corroboration for the same person, a query
assembles the verification work already furnished by network participants into a
single network-issued certificate.
| | |
| -------------- | -------------------------------------------------------------------------------------- |
| **Category** | Identity Verification |
| **Use case** | Customer onboarding |
| **Subject** | Consumer |
| **Family** | Certificate (issues a `certificate_id`) |
| **Operations** | `POST /v1/products/kyc_certificate/query`, `POST /v1/products/kyc_certificate/furnish` |
## What's in the certificate
A KYC certificate consolidates up to nine **sub-products**, each a block in the
query response with its own `assertions` (what was attested, and when) and
`data` (the supporting attributes):
| Sub-product | Response key | What it attests |
| ---------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
| Document capture | `document_capture` | An identity document was captured, with its type, number, and dates. |
| Document review | `document_review` | The document's attributes were reviewed — tamper and machine-readable-data checks. |
| Biometric capture | `biometric_capture` | A biometric artifact (e.g. a selfie) was captured. |
| Biometric review | `biometric_review` | The biometric was compared against a reference, with quality and outcome. |
| Liveness capture | `liveness_capture` | Liveness evidence was captured. |
| Liveness review | `liveness_review` | The liveness evidence was reviewed, with outcome and confidence tier. |
| Address capture | `address_capture` | A residential address was captured. |
| Address verification | `address_verification` | The address was verified, with methods and per-source match counts. |
| Identity corroboration | `identity_corroboration` | CIP-style corroboration — SSN, name, and DOB matches across sources. |
Every populated sub-product carries the `furnishing_entity_id` of the
participant whose data backed it and the `attestation_id` of their attestation,
so the certificate is auditable down to its sources.
## At a glance
```bash theme={null}
curl -X POST https://api.solo.one/v1/products/kyc_certificate/query \
-H "Authorization: Bearer $SOLO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent_id": "a3f0b9c7-…",
"policy_id": "5e7d2a14-…",
"network_ids": ["9f1c0c2e-…"]
}'
```
A `200 OK` means a certificate was issued — the response carries a
`certificate_id`, the billable `query_event_id`, and a `result` block with each
requested sub-product (sub-products the policy didn't require, or that lacked
data, are `null`). A `204 No Content` means the available data did not satisfy
the policy.
The complete KYC certificate deep dive — every model, the full response
example, how it resolves per network, and the `204` rules.
## Related
The full product catalog.
Check field coverage before running a billable query.
# Products
Source: https://docs.solo.one/products/overview
The standardized data sets you query and furnish on the SOLO network
A **product** is a standardized, named data set you can **query** (read) and —
for certificate products — **furnish** (write) through the network. Every
product gives all participants a common schema for one verification domain, so
data furnished by one participant can be queried by another without bespoke
integration.
This tab is the high-level outline of each product. For request anatomy, `200`
vs `204` semantics, billing, and the `X-Ref-Id` header, see
[Network consumption](/api-overview/querying/overview).
## The catalog
SOLO offers **five products**, in two categories. The exact set available to you
depends on the [networks](/concepts/governance/networks) you belong to.
| Product | Category | Subject | What it answers |
| ------------------------------------------------------------------------------------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| [KYC Certificate](/products/kyc-certificate) | Identity Verification | Consumer | "Has this consumer's identity been verified — documents, biometrics, liveness, address, and corroboration?" |
| [KYB Certificate](/products/kyb-certificate) | Identity Verification | Business | "Has this business been verified — identity, ownership & control, and risk/compliance?" |
| [Bank-Specific Bad Actor List](/products/bank-specific-bad-actor-list) | Fraud & Financial Crime | Consumer | "Has this consumer violated a documented policy of this sponsor bank's subnetwork?" |
| [Cross-Bank Financial Crimes Watch List](/products/cross-bank-financial-crimes-watch-list) | Fraud & Financial Crime | Consumer | "Is this consumer associated with suspicious financial-crimes activity reported across participating banks?" |
| [Confirmed Fraud Attribute List](/products/confirmed-fraud-attribute-list) | Fraud & Financial Crime | Consumer | "Is this consumer tied to a confirmed fraud event, and which identifying attributes were implicated?" |
## Two families
| Behavior | Certificates | Lists |
| -------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- |
| Examples | KYC Certificate, KYB Certificate | Bad Actor List, Financial Crimes Watch List, Confirmed Fraud Attribute List |
| What you get | A consolidated, multi-attribute verification result | A listing indicator plus a small set of context fields |
| Furnishable via API | Yes — `/furnish` | No (query-only) |
| Issues an artifact | Yes — `certificate_id` | No |
| Empty result | `204 No Content` | `200` with `is_listed: false` |
| Billable query event | Yes | Yes |
* **Certificates** are consolidated verification results assembled from data
furnished by network participants. Querying one can *issue* a reusable
certificate for the subject, and returns `204 No Content` when the available
data cannot satisfy the policy.
* **Lists** are read-only yes/no checks against flagged individuals. They always
return `200 OK`, with a clean result expressed as `"is_listed": false`.
## The query / furnish pattern
**Read** consolidated data for an entity, drawn from what authorized
participants have furnished. Requires a [consent](/concepts/identity/consent)
ID or a direct profile reference.
**Contribute** verified data for an entity into a network, making it
available for future queries by entitled participants.
```text theme={null}
POST /v1/products/{product}/query
POST /v1/products/{product}/furnish (certificate products)
```
There is also one utility endpoint that is **not** a product query:
`POST /v1/products/check`, the non-billable
[coverage check](/products/coverage-check), which reports whether the furnished
data for a subject can satisfy a policy *before* you run a billable query.
## How products relate to networks and policies
```mermaid theme={null}
flowchart LR
Prod[Product
schema] --> NP[Offered in a network]
Net[Network] --> NP
NP --> Pol[Querying policy
field rules + freshness]
Pol --> Q[Query result]
```
A product defines *what* data exists — its models, fields, and types. A
[network](/concepts/governance/networks) decides *which* products it offers to
its members. A [querying policy](/concepts/governance/querying-policies)
defines, per network, *which parts* of a product a query may read and under what
conditions. The same product can behave differently in two networks because
each network attaches its own policies.
## Choosing a product
| If you need to… | Use |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Verify a consumer's identity at onboarding | [KYC Certificate](/products/kyc-certificate) |
| Verify a business and its beneficial owners | [KYB Certificate](/products/kyb-certificate) |
| Screen a consumer against your sponsor bank's subnetwork history | [Bank-Specific Bad Actor List](/products/bank-specific-bad-actor-list) |
| Screen a consumer for cross-bank financial-crimes signals | [Cross-Bank Financial Crimes Watch List](/products/cross-bank-financial-crimes-watch-list) |
| Screen a consumer against confirmed-fraud attributes | [Confirmed Fraud Attribute List](/products/confirmed-fraud-attribute-list) |
| Know whether a query can succeed before paying for it | [Coverage Check](/products/coverage-check) |
## Product deep dives
Consolidated consumer identity verification — up to nine sub-products.
Business identity, ownership & control, and risk/compliance in one
certificate.
Sponsor-bank-scoped repeat-offender screening.
314(b) consortium financial-crimes signals.
Discrete identifying attributes tied to confirmed fraud events.
Non-billable pre-flight: can this query succeed under this policy?