From c61a67f13f305f912e463b5c5675b48e7783e5ef Mon Sep 17 00:00:00 2001 From: Jason Wang Date: Thu, 13 Aug 2026 15:53:18 -0700 Subject: [PATCH] docs(kyc): add a KYC/KYB verification page covering both onboarding paths The API reference documented the hosted link flow only in passing, and only for individual customers. Integrators had no way to discover that the same link endpoint runs KYB for business customers, or how to choose between the hosted flow and submitting verification data through the API. Add an "KYC & KYB verification" page to the API reference that leads with the choice between the two paths and then covers what each one needs: - A comparison of the hosted link and direct API paths, noting that both cover KYC and KYB, produce the same status transitions and webhooks, and can be mixed. - Hosted link generation, including that POST /customers/{id}/kyc-link serves both customer types and that customerType selects the flow. - Required versus optional fields for creating a business customer, and the three groups (contact channels, currency-driven, review-driven) that are optional to the schema but required in practice. - kycStatus/kybStatus transitions and the CUSTOMER.KYC_* / CUSTOMER.KYB_* webhooks. - The sandbox registration-number suffixes, extracted into a shared snippet so the sandbox testing page and the new page stay in sync. Document the 003 suffix explicitly: any other suffix auto-approves a business customer on the spot, leaving the hosted flow with nothing to verify, so it requests no documents and no beneficial owners. That reads as a broken KYB flow but isn't, and 003 is what avoids it. Also broaden the kyc-link endpoint description to cover the KYB case and point at the direct API alternative. Co-Authored-By: Claude Opus 5 (1M context) --- .../api-reference/kyc-kyb-verification.mdx | 10 ++ mintlify/docs.json | 1 + mintlify/openapi.yaml | 8 +- .../snippets/kyc/kyb-sandbox-suffixes.mdx | 14 ++ .../snippets/kyc/verification-options.mdx | 127 ++++++++++++++++++ mintlify/snippets/sandbox-verification.mdx | 12 +- openapi.yaml | 8 +- .../customers_{customerId}_kyc-link.yaml | 8 +- 8 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 mintlify/api-reference/kyc-kyb-verification.mdx create mode 100644 mintlify/snippets/kyc/kyb-sandbox-suffixes.mdx create mode 100644 mintlify/snippets/kyc/verification-options.mdx diff --git a/mintlify/api-reference/kyc-kyb-verification.mdx b/mintlify/api-reference/kyc-kyb-verification.mdx new file mode 100644 index 000000000..5e2f9ba46 --- /dev/null +++ b/mintlify/api-reference/kyc-kyb-verification.mdx @@ -0,0 +1,10 @@ +--- +title: "KYC & KYB verification" +icon: "/images/icons/suitcase-work.svg" +description: "Verify individual and business customers through a hosted link or directly through the API — options, required fields, status transitions, and webhooks" +"og:image": "/images/og/og-api-reference.png" +--- + +import VerificationOptions from '/snippets/kyc/verification-options.mdx'; + + diff --git a/mintlify/docs.json b/mintlify/docs.json index 41d5b8ec8..f69b161de 100644 --- a/mintlify/docs.json +++ b/mintlify/docs.json @@ -394,6 +394,7 @@ "api-reference/terminology", "api-reference/authentication", "api-reference/webhooks", + "api-reference/kyc-kyb-verification", "api-reference/sandbox-testing", "api-reference/sdks" ] diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 0fef08e37..7352105c1 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -868,11 +868,15 @@ paths: schema: type: string post: - summary: Generate a hosted KYC link for an existing customer + summary: Generate a hosted KYC/KYB link for an existing customer description: | Generate a single-use hosted URL the customer can complete to verify their identity, and (where supported) a provider-specific `token` for embedding the verification flow directly via the provider's SDK. - The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's `kycStatus`; the customer remains `PENDING` until they complete (or fail) the hosted flow. + The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's verification status; the customer remains at their current status until they complete (or fail) the hosted flow. + + This endpoint generates the link for both customer types; `customerType` selects which flow the provider runs. `INDIVIDUAL` runs identity verification (KYC), tracked on `kycStatus`. `BUSINESS` runs business verification (KYB), tracked on `kybStatus` — the flow confirms the company details, collects formation, ownership, and proof-of-address documents, and gathers the control person and every beneficial owner holding 25% or more. Business information already supplied via `POST /customers` or `PATCH /customers/{customerId}` is prefilled, so send what you have before generating the link. + + The hosted link is one of two ways to verify a customer. To collect the data yourself instead, submit it through `POST /customers`, `POST /beneficial-owners` (business customers), and `POST /documents`, then call `POST /verifications`. Both paths produce the same status transitions and the same `CUSTOMER.KYC_*` / `CUSTOMER.KYB_*` webhooks. Each call returns a fresh link. Previously-issued links are not invalidated, but they remain single-use and will expire on their own. For request-level retry safety, include an `Idempotency-Key` header. operationId: createCustomerKycLink diff --git a/mintlify/snippets/kyc/kyb-sandbox-suffixes.mdx b/mintlify/snippets/kyc/kyb-sandbox-suffixes.mdx new file mode 100644 index 000000000..8d6a516d2 --- /dev/null +++ b/mintlify/snippets/kyc/kyb-sandbox-suffixes.mdx @@ -0,0 +1,14 @@ +Business customers are always created with `kybStatus: UNVERIFIED`. The **last 3 characters** of `businessInfo.registrationNumber` decide what happens when the business is verified — whether you submit with `POST /verifications` or send the business through a hosted KYB link: + +| Suffix | Outcome | +|--------|---------| +| **003** | No auto-decision. Document and UBO verification run for real — the hosted flow asks for company documents and beneficial owners, and `POST /verifications` applies normal validation (`RESOLVE_ERRORS` until the data is complete). **Use this to test the link flow.** | +| **001** | Same as `003` — no auto-decision, normal validation applies | +| **002** | Immediate `kybStatus: REJECTED` (`verificationStatus: REJECTED`), skipping data and document validation | +| **Any other** | Immediate `kybStatus: APPROVED` (`verificationStatus: APPROVED`), skipping data and document validation | + + +A registration number that doesn't end in `001`, `002`, or `003` is **auto-approved on the spot**. The hosted flow then has nothing left to verify, so it asks for no documents and no beneficial owners — which looks exactly like a broken KYB flow but isn't. Always use a `003` suffix when you want to exercise document and UBO collection. + + +Once a business customer is approved or rejected, further `POST /verifications` calls return `400`. diff --git a/mintlify/snippets/kyc/verification-options.mdx b/mintlify/snippets/kyc/verification-options.mdx new file mode 100644 index 000000000..52ab9724f --- /dev/null +++ b/mintlify/snippets/kyc/verification-options.mdx @@ -0,0 +1,127 @@ +import KybDataRequirements from '/snippets/kyc/kyb-data-requirements.mdx' +import KybSandboxSuffixes from '/snippets/kyc/kyb-sandbox-suffixes.mdx' + +Grid verifies individual customers with KYC and business customers with KYB. Which applies is decided by `customerType`, and the result lands on `kycStatus` for individuals and `kybStatus` for businesses. + +**Regulated platforms** run verification through their own compliance systems and create customers directly with `POST /customers`. **Unregulated platforms** have Grid verify, using either of the two paths below. + +## Your options + +Both paths cover KYC and KYB, produce the same status transitions, and emit the same webhooks. You can mix them — supply what you already hold through the API, then let the hosted flow collect the rest. + +| | Hosted link | Direct API | +|---|---|---| +| Who collects the data | Grid's hosted flow, or the provider SDK embedded in your UI | You, in your own UI | +| Identity documents | Uploaded by the customer in the flow | `POST /documents` | +| Beneficial owners (KYB) | Declared by the applicant in the flow | `POST /beneficial-owners` | +| Submission | Automatic when the customer finishes | `POST /verifications` | +| Resolving missing data | The customer, inside the flow | You, from the returned `errors` array | +| Best when | You want Grid to own the collection UX | You already collect this data, or need it in your own UI | + +## Hosted link + +Create the customer, then call `POST /customers/{customerId}/kyc-link`. There's no dedicated KYB link endpoint — this one serves both types, and `customerType` selects which flow the provider runs. + +```bash +curl -X POST "https://api.lightspark.com/grid/2025-10-13/customers/Customer:019542f5-b3e7-1d02-0000-000000000001/kyc-link" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: $(uuidgen)" \ + -d '{ "redirectUri": "https://yourapp.com/onboarding-complete" }' +``` + +```json +{ + "kycUrl": "https://kyc.lightspark.com/onboard/abc123def456", + "expiresAt": "2027-01-15T14:32:00Z", + "provider": "SUMSUB", + "token": "_act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +Send the customer to `kycUrl`, or pass `token` to the provider's SDK to embed the flow in your own UI. The field is named `kycUrl` for both customer types; for a `BUSINESS` customer it opens the KYB flow, where the applicant confirms the company details, uploads the formation and ownership documents, and declares the control person and every beneficial owner holding 25% or more. + + + + + +- Links are single-use and expire at `expiresAt`. Each call mints a fresh one; earlier links aren't invalidated. `redirectUri` is optional and must be `https://`. +- Generating a link doesn't change the customer's status — that happens when they submit. +- A `409` means contact verification is incomplete. When the customer carries a `contactVerification` object, every channel it lists must reach `VERIFIED` first, via `POST /customers/{customerId}/verify-email` / `verify-phone` and their `/confirm` sub-routes. When the object is absent, nothing is required. +- Business information you supply via `POST /customers` and `PATCH /customers/{customerId}` is prefilled into the flow, so send as much as you have **before** generating the link. + + +Reaching your `redirectUri` means the customer finished the flow, not that they were approved. Wait for the decision. + + +## Direct API + +Submit the data yourself: `POST /customers`, then `POST /beneficial-owners` for business customers, `POST /documents` for identity and company documents, and finally `POST /verifications`. If anything is missing, `verificationStatus` comes back as `RESOLVE_ERRORS` with one `errors` entry per problem — fix them and resubmit. + +For the full walkthrough, see [Configuring customers](/payouts-and-b2b/onboarding/configuring-customers). + +## Creating a business customer + +Either path starts here. `POST /customers` with `customerType: BUSINESS` requires: + +| Field | Notes | +|-------|-------| +| `businessInfo.legalName` | Full legal entity name | +| `businessInfo.country` | Country of incorporation, ISO 3166-1 alpha-2. Sets the applicant's jurisdiction and the tax-ID format validated against | +| `businessInfo.taxId` | Validated against `businessInfo.country` | +| `businessInfo.incorporatedOn` | `YYYY-MM-DD` | + +Everything else is optional to the schema, but three groups matter in practice: + +- **Contact channels** — `email` and/or `phoneNumber`, plus `businessInfo.primaryContactFirstName` and `primaryContactLastName`, are required in regions that verify a named representative before verification begins (for example the EU). +- **Currency-driven fields** — `GET /config` returns `supportedCurrencies`, each with a `providerRequiredCustomerFields` list. Anything listed for a currency the business will use must be supplied. +- **Everything the review needs** — `address`, `registrationNumber`, `entityType`, `countriesOfOperation`, `businessType`, `purposeOfAccount`, `sourceOfFunds`, and the expected-activity fields aren't enforced at creation. Whatever you omit, the applicant is asked for in the hosted flow. + +Individual customers need `customerType: INDIVIDUAL`; `fullName` must contain both a given and a family name. + +## Status transitions + +`kycStatus` and `kybStatus` share the same values and the same path: `UNVERIFIED` → `PENDING` → `APPROVED` / `REJECTED`. + +| Status | Meaning | +|--------|---------| +| `UNVERIFIED` | Created, not yet submitted. Generating a link does not move it off this value | +| `PENDING` | Submitted; review under way | +| `APPROVED` | Passed — unlock funding and money movement | +| `REJECTED` | Failed | +| `HOLD` | On hold; the customer may be asked to supply more information | + +While the status is `PENDING`, let the customer finish account setup but block funding and money movement. + +`GET /verifications?customerId=...` gives the finer-grained `verificationStatus` (`RESOLVE_ERRORS`, `IN_PROGRESS`, `PENDING_MANUAL_REVIEW`, `APPROVED`, `REJECTED`, `READY_FOR_VERIFICATION`) and the `errors` array. That detail drives the direct API path; in the hosted flow the customer resolves it inside the flow, so integrate against `kycStatus` / `kybStatus`. + +## Webhooks + +| Event | Fires when | +|-------|-----------| +| `CUSTOMER.KYC_PENDING` / `CUSTOMER.KYB_PENDING` | Submitted for review — use it to show an "under review" state | +| `CUSTOMER.KYC_APPROVED` / `CUSTOMER.KYB_APPROVED` | Terminal: passed | +| `CUSTOMER.KYC_REJECTED` / `CUSTOMER.KYB_REJECTED` | Terminal: failed | + +The `KYB_*` events fire only for `customerType: BUSINESS`. `data` is the full customer resource, identical to `GET /customers/{customerId}`: + +```json +{ + "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007", + "type": "CUSTOMER.KYB_APPROVED", + "timestamp": "2025-08-15T14:32:00Z", + "data": { + "id": "Customer:019542f5-b3e7-1d02-0000-000000000001", + "customerType": "BUSINESS", + "kybStatus": "APPROVED" + } +} +``` + +Verify `X-Grid-Signature` against the raw request body and deduplicate on the webhook `id`. The `VERIFICATION.*` events carry the verification-level detail if you want it. To poll instead, read the status off `GET /customers/{customerId}`. + +## Sandbox + + + +For individual customers and beneficial owners, the equivalent suffixes live on `fullName` and on each owner's last name — see [Sandbox testing](/api-reference/sandbox-testing). diff --git a/mintlify/snippets/sandbox-verification.mdx b/mintlify/snippets/sandbox-verification.mdx index 4e1827fed..c13d1cd9d 100644 --- a/mintlify/snippets/sandbox-verification.mdx +++ b/mintlify/snippets/sandbox-verification.mdx @@ -1,3 +1,5 @@ +import KybSandboxSuffixes from '/snippets/kyc/kyb-sandbox-suffixes.mdx' + In sandbox, you can trigger specific KYC/KYB verification outcomes using magic suffixes in customer and beneficial owner fields. These let you test different verification flows without waiting for real review. ### Individual customer verification (KYC) @@ -104,15 +106,9 @@ Fix-and-resubmit example — the standard integration loop: ### Business customer verification (KYB) -Business customers are always created with `kybStatus: UNVERIFIED` — their suffix applies when you call `POST /verifications`, not at creation. The **last 3 characters** of the `registrationNumber` in `businessInfo` determine the outcome: - -| Suffix | Outcome on `POST /verifications` | -|--------|----------------------------------| -| **002** | `kybStatus: REJECTED`, `verificationStatus: REJECTED` — immediate, skips data and document validation | -| **001** / **003** | Normal validation applies: complete business information, business documents, and at least one beneficial owner are required (`RESOLVE_ERRORS` otherwise); a complete submission stays `PENDING` | -| **Any other** | `kybStatus: APPROVED`, `verificationStatus: APPROVED` — immediate, skips data and document validation | + -Once a business customer is approved or rejected, further `POST /verifications` calls return `400`. +For direct API onboarding, `001` and `003` both require complete business information, business documents, and at least one beneficial owner before `POST /verifications` moves past `RESOLVE_ERRORS`. For the hosted flow, see [KYC & KYB verification](/api-reference/kyc-kyb-verification). ### Beneficial owner KYC diff --git a/openapi.yaml b/openapi.yaml index 0fef08e37..7352105c1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -868,11 +868,15 @@ paths: schema: type: string post: - summary: Generate a hosted KYC link for an existing customer + summary: Generate a hosted KYC/KYB link for an existing customer description: | Generate a single-use hosted URL the customer can complete to verify their identity, and (where supported) a provider-specific `token` for embedding the verification flow directly via the provider's SDK. - The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's `kycStatus`; the customer remains `PENDING` until they complete (or fail) the hosted flow. + The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's verification status; the customer remains at their current status until they complete (or fail) the hosted flow. + + This endpoint generates the link for both customer types; `customerType` selects which flow the provider runs. `INDIVIDUAL` runs identity verification (KYC), tracked on `kycStatus`. `BUSINESS` runs business verification (KYB), tracked on `kybStatus` — the flow confirms the company details, collects formation, ownership, and proof-of-address documents, and gathers the control person and every beneficial owner holding 25% or more. Business information already supplied via `POST /customers` or `PATCH /customers/{customerId}` is prefilled, so send what you have before generating the link. + + The hosted link is one of two ways to verify a customer. To collect the data yourself instead, submit it through `POST /customers`, `POST /beneficial-owners` (business customers), and `POST /documents`, then call `POST /verifications`. Both paths produce the same status transitions and the same `CUSTOMER.KYC_*` / `CUSTOMER.KYB_*` webhooks. Each call returns a fresh link. Previously-issued links are not invalidated, but they remain single-use and will expire on their own. For request-level retry safety, include an `Idempotency-Key` header. operationId: createCustomerKycLink diff --git a/openapi/paths/customers/customers_{customerId}_kyc-link.yaml b/openapi/paths/customers/customers_{customerId}_kyc-link.yaml index e077e5ccd..e9fd954ad 100644 --- a/openapi/paths/customers/customers_{customerId}_kyc-link.yaml +++ b/openapi/paths/customers/customers_{customerId}_kyc-link.yaml @@ -6,11 +6,15 @@ parameters: schema: type: string post: - summary: Generate a hosted KYC link for an existing customer + summary: Generate a hosted KYC/KYB link for an existing customer description: | Generate a single-use hosted URL the customer can complete to verify their identity, and (where supported) a provider-specific `token` for embedding the verification flow directly via the provider's SDK. - The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's `kycStatus`; the customer remains `PENDING` until they complete (or fail) the hosted flow. + The customer must already exist — create them with `POST /customers` first. Calling this endpoint does not change the customer's verification status; the customer remains at their current status until they complete (or fail) the hosted flow. + + This endpoint generates the link for both customer types; `customerType` selects which flow the provider runs. `INDIVIDUAL` runs identity verification (KYC), tracked on `kycStatus`. `BUSINESS` runs business verification (KYB), tracked on `kybStatus` — the flow confirms the company details, collects formation, ownership, and proof-of-address documents, and gathers the control person and every beneficial owner holding 25% or more. Business information already supplied via `POST /customers` or `PATCH /customers/{customerId}` is prefilled, so send what you have before generating the link. + + The hosted link is one of two ways to verify a customer. To collect the data yourself instead, submit it through `POST /customers`, `POST /beneficial-owners` (business customers), and `POST /documents`, then call `POST /verifications`. Both paths produce the same status transitions and the same `CUSTOMER.KYC_*` / `CUSTOMER.KYB_*` webhooks. Each call returns a fresh link. Previously-issued links are not invalidated, but they remain single-use and will expire on their own. For request-level retry safety, include an `Idempotency-Key` header. operationId: createCustomerKycLink