# 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 Subnetwork workbook upload dialog Subnetwork workbook upload — ready to submit Subnetwork workbook upload accepted Entity KYC data workbook upload — ready to submit Entity KYC data workbook upload accepted — dialog closed Entity KYB data workbook upload — ready to submit Entity KYB data workbook upload accepted — dialog closed File uploads list (workbook ingest status) ## 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 Furnishing subnetworks list — before subnetwork workbook upload Subnetwork workbook upload dialog Furnishing channels — REST API, SFTP file drops, and data lake connectors Product furnish onboarding — step overview Institutional attestation — active or ready to certify Product furnish onboarding — KYC data workbook ready to upload Product furnish onboarding — KYC upload accepted and processing KYC product detail after successful furnish onboarding Consumer detail — furnishments tab Entity furnish — product picker dialog Entity furnish — KYC product selected Furnished consumer — furnishments tab after KYC upload Business detail — furnishments tab Entity furnish — KYB product picker Entity furnish — KYB product selected Furnished business — furnishments tab after KYB upload Furnish events history list Furnish event detail — overview tab ## 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 Run query — choose consumer or business Run query — choose KYB certificate product Run query — choose policy Run query — choose business Run query — networks and consent matrix Business KYB query — policy and billing Business KYB query — REST response payload Business — query history tab 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 Run query — choose KYC certificate product Run query — choose policy Run query — choose entity Run query — networks and consent matrix Query completed — run detail overview Consumer KYC certificate — parsed result fields Consumer KYC query — overview tab 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 Run query — choose consumer or business Run query — choose KYC certificate product Run query — choose policy Run query — choose entity Run query — networks and consent matrix Query completed — run detail overview Consumer KYC query — overview tab Query history list — recent consumer query Query history — consumer KYC run detail opened from list Business KYB query — REST response payload Consumer — query history tab ## 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 Products catalogue — By Templates Products catalogue — By Attributes tab Run query — choose KYC certificate product KYC product detail — overview KYC product detail — Querying Policies tab Run query opened from product detail Run query — choose KYB certificate product ## 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 Furnishing channels — REST API, SFTP file drops, and data lake connectors 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 KYC product detail — furnishing policies tab Furnishing policies list Create KYC furnishing policy — ready to submit KYC furnishing policy created — detail page Activate furnishing policy Furnishing policies list — KYC and KYB policies visible KYB product detail — Furnishing Policies tab ## 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 Network created — detail page with new network Network detail — overview *** **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 Create network — review and submit Network created — detail page with new network Network detail — topology graph Networks list — newly created network visible Topbar network multiselect open ## 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 Querying policies list Create querying policy — ready to submit Querying policy created — detail page Querying policy field configuration — edit mode KYC product detail — Querying Policies tab Run query opened from policy detail Run query — choose entity Run query — Network & Furnishers tab Query completed — run detail overview ## 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 Consumer — consent tab Find or create consumer dialog Find or create consumer — ready to submit Find or create business dialog Find or create business — ready to submit Business — consent tab ## 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 Entities hub — All tab Entities hub — Consumers tab Entities hub — Businesses tab Consumers list Consumer consent created — profile detail Consumer — identity verification tab Furnished consumer — profile detail Businesses list Business profile — overview Furnished business — profile detail ## 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 Home — querying and furnishing checklists 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 Networks list — newly created network visible Topbar network multiselect open Network detail — overview Network detail — topology graph 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 Networks list — newly created network visible Topbar network multiselect open 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 Products catalogue — By Templates KYC product detail — overview tab Run query opened from product detail Run query — choose entity Run query — networks and consent matrix Query completed — run detail overview 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?