Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mintlify/api-reference/kyc-kyb-verification.mdx
Original file line number Diff line number Diff line change
@@ -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';

<VerificationOptions />
1 change: 1 addition & 0 deletions mintlify/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
Expand Down
8 changes: 6 additions & 2 deletions mintlify/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions mintlify/snippets/kyc/kyb-sandbox-suffixes.mdx
Original file line number Diff line number Diff line change
@@ -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 |

<Warning>
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.
</Warning>

Once a business customer is approved or rejected, further `POST /verifications` calls return `400`.
127 changes: 127 additions & 0 deletions mintlify/snippets/kyc/verification-options.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Accordion title="What the KYB flow collects">
<KybDataRequirements />
</Accordion>

- 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.

<Warning>
Reaching your `redirectUri` means the customer finished the flow, not that they were approved. Wait for the decision.
</Warning>

## 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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Approval omits the consent gate

When an unregulated platform has not recorded End User Terms consent, this row incorrectly presents KYC/KYB approval as sufficient to unlock funding and money movement, causing customer-scoped transactions to be rejected despite the documented guidance.

Suggested change
| `APPROVED` | Passed — unlock funding and money movement |
| `APPROVED` | Passed verification. For unregulated platforms, record End User Terms consent before enabling funding and money movement |

Knowledge Base Used: Customers and KYC

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/kyc/verification-options.mdx
Line: 90

Comment:
**Approval omits the consent gate**

When an unregulated platform has not recorded End User Terms consent, this row incorrectly presents KYC/KYB approval as sufficient to unlock funding and money movement, causing customer-scoped transactions to be rejected despite the documented guidance.

```suggestion
| `APPROVED` | Passed verification. For unregulated platforms, record End User Terms consent before enabling funding and money movement |
```

**Knowledge Base Used:** [Customers and KYC](https://app.greptile.com/lightspark/-/custom-context/knowledge-base/lightsparkdev/grid-api/-/docs/customers-and-kyc.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

| `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

<KybSandboxSuffixes />

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).
12 changes: 4 additions & 8 deletions mintlify/snippets/sandbox-verification.mdx
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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 |
<KybSandboxSuffixes />

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

Expand Down
8 changes: 6 additions & 2 deletions openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions openapi/paths/customers/customers_{customerId}_kyc-link.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading