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