refactor(customers): redesign KYC link endpoint as POST /customers/{customerId}/kyc-link#457
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
✱ Stainless preview builds for gridThis PR will update the kotlin openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs.
|
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-link` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /customers/kyc-links` |
| ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-links/{kycLinkId}` |
| 💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member. |
⚠️ grid-openapi studio · code · diff
Your SDK build had at least one "error" diagnostic, which is a regression from the base state.
generate ❗(prev:generate ✅)New diagnostics (3 error)
❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-link` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /customers/kyc-links` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-links/{kycLinkId}`
⚠️ grid-typescript studio · code · diff
Your SDK build had at least one "error" diagnostic, which is a regression from the base state.
generate ❗(prev:generate ✅) →build ✅→lint ❗→test ✅npm install https://pkg.stainless.com/s/grid-typescript/1a4ffbd5b4a76535fca63cf0058f085dac7e07d4/dist.tar.gzNew diagnostics (3 error)
❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-link` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /customers/kyc-links` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-links/{kycLinkId}`
⚠️ grid-python studio · code · diff
Your SDK build had at least one "error" diagnostic, which is a regression from the base state.
generate ❗(prev:generate ✅) →build ✅→lint ✅→test ✅pip install https://pkg.stainless.com/s/grid-python/520378645445c1b9a12d75c8c9936c8694980974/grid-0.0.1-py3-none-any.whlNew diagnostics (3 error)
❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-link` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `post /customers/kyc-links` ❗ Endpoint/NotFound: Skipped endpoint because it's not in your OpenAPI spec: `get /customers/kyc-links/{kycLinkId}`
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-05-11 22:38:04 UTC
|
@greptile review this |
Greptile SummaryThis PR replaces the old
Confidence Score: 5/5Safe to merge — the changes are purely additive to the OpenAPI spec and docs, with no application logic to break. The new endpoint is cleanly scoped to an existing customer, the response schema has correct required fields, the docs accurately describe the two-step flow, and the lint-ignore suppression for the path ambiguity is correct. The only open items are a missing 409 response for idempotency key conflicts and a PR description that does not match what was actually implemented — neither affects the correctness of the spec or docs as shipped. The PR description (not a tracked file) misrepresents the endpoint path and response schema; worth updating before the PR is used as a reference by other engineers.
|
| Filename | Overview |
|---|---|
| openapi/paths/customers/customers_{customerId}_kyc-link.yaml | New POST endpoint for generating a hosted KYC link on an existing customer; well-structured with idempotency support, 404 handling, and clear description — missing a 409 response for idempotency key conflicts and the PR description does not match this implementation. |
| openapi/components/schemas/customers/KycLinkResponse.yaml | Replaces old three-field response with kycUrl, expiresAt, provider (all required), and optional token; required array now present, future-dated example timestamp. |
| openapi/components/schemas/customers/KycLinkCreateRequest.yaml | Minimal request schema with a single optional redirectUri; intentionally body-optional to allow bare POST calls that generate a link without a redirect. |
| .stainless/stainless.yml | SDK config updated to replace get_kyc_link with create_kyc_link pointing to the new POST endpoint; new schemas registered correctly. |
| cli/src/commands/customers.ts | CLI command updated to accept customerId as a positional arg and POST to the new endpoint; local KycLinkResponse interface matches the new schema. |
| mintlify/snippets/kyc/kyc-unregulated.mdx | Docs rewritten to the two-step flow; accurately explains that redirect is not an approval signal and covers both webhook and polling status tracking. |
Sequence Diagram
sequenceDiagram
participant P as Platform
participant G as Grid API
participant K as KYC Provider
participant C as Customer
P->>G: POST /customers (customerType, email, region)
G-->>P: 201 id and kycStatus PENDING
P->>G: POST /customers/Customer:xxx/kyc-link (redirectUri, Idempotency-Key)
G->>K: Create hosted verification session
K-->>G: kycUrl and token
G-->>P: 201 kycUrl, expiresAt, provider, token
P->>C: Redirect to kycUrl or embed via token
C->>K: Complete identity verification
K-->>C: Redirect to redirectUri
alt Webhook recommended
K->>G: Verification decision
G->>P: KYC_STATUS webhook APPROVED or REJECTED
else Polling
P->>G: GET /customers/Customer:xxx
G-->>P: kycStatus APPROVED
end
P->>P: Unlock funding on APPROVED
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
openapi/paths/customers/customers_{customerId}_kyc-link.yaml:22-30
**Missing 409 response for idempotency key conflicts**
The `Idempotency-Key` header is documented as "if the same key is sent multiple times, the server will return the same response as the first request," but there is no `409 Conflict` response defined for the case where the same key is replayed with a _different_ request body. Most idempotency implementations return 409 in that scenario, and without a documented response callers have no way to know what error shape to expect or how to distinguish a payload mismatch from a server error.
### Issue 2 of 2
openapi/paths/customers/customers_{customerId}_kyc-link.yaml:1-10
**PR description describes a different API than what is implemented**
The PR summary states the endpoint is `POST /customers/kyc-links` (no `customerId` in the path, creates the customer atomically), and claims the response will include `kycLinkId`, `kycStatus`, and `createdAt`, plus a new `KycLinkStatus` enum and a `GET /customers/kyc-links/{kycLinkId}` polling endpoint. The actual implementation is `POST /customers/{customerId}/kyc-link` (requires a pre-existing customer), the response schema has `kycUrl`/`expiresAt`/`provider`/`token`, and no GET status-poll endpoint is added.
Is the PR description a carry-over from an earlier design iteration, or does it reflect work planned for a follow-up PR? Clarifying this prevents reviewers from approving based on an inaccurate spec description.
Reviews (4): Last reviewed commit: "refactor(customers): redesign KYC link e..." | Re-trigger Greptile
3e6dbe1 to
8b43eb1
Compare
|
@greptile review |
8b43eb1 to
33dacdf
Compare
33dacdf to
ebf457f
Compare
…ustomerId}/kyc-link
## Summary
Replaces `GET /customers/kyc-link` with **`POST /customers/{customerId}/kyc-link`** — a nested action on an existing customer. Customer creation and KYC link generation are now two separate steps.
## Motivation
Previous iterations of this PR explored two designs:
1. `GET /customers/kyc-link` (current production) — wrong verb (it creates resources), required `platformCustomerId` overloaded as both customer-uniqueness key and idempotency key, no way to shape the hosted flow, no way to check status without webhooks.
2. `POST /customers/kyc-links` returning a `kycLinkId` resource (first iteration) — better, but conflated customer creation with link generation. The request body had to duplicate every field on the customer schema (`customerType`, `region`, `currencies`, `email`, etc.).
After reviewer feedback, this PR lands on the cleanest shape: **create the customer first with `POST /customers`, then call `POST /customers/{customerId}/kyc-link` to generate a link for that customer.** The request body becomes trivial (just optional `redirectUri`), the response is minimal, and there's no addressable `kycLinkId` resource to manage. Status is read off the customer's existing `kycStatus` field; the webhook contract is unchanged.
## Endpoint shape
```
POST /customers/{customerId}/kyc-link
Headers: Idempotency-Key: <uuid>
Body (optional):
{ "redirectUri": "https://app.example.com/onboarding/completed" }
Response 201:
{
"kycUrl": "https://kyc.lightspark.com/onboard/abc123",
"expiresAt": "2027-01-15T14:32:00Z",
"provider": "SUMSUB",
"token": "_act-sbx-jwt-..." // optional, only for providers with SDK support
}
```
Each call returns a fresh single-use URL. Previously-issued links remain single-use but aren't invalidated — the platform can call again any time the customer needs a new link.
`provider` exposes the KYC provider Grid is using behind the scenes; the optional `token` lets platforms embed the provider's SDK directly (e.g. Sumsub Web SDK) instead of redirecting to the hosted URL. Both paths update the same `kycStatus` on the customer.
## Changes
**OpenAPI (`openapi/`)**
- New `POST /customers/{customerId}/kyc-link` (`createCustomerKycLink`).
- New schemas: `KycLinkResponse` (4 fields, `required: [kycUrl, expiresAt, provider]`), `KycProvider` enum (`SUMSUB`).
- Removed: `KycLinkCreateRequest`, `KycLinkStatus`, the old `GET /customers/kyc-link` path, and the intermediate `POST /customers/kyc-links` + `GET /customers/kyc-links/{kycLinkId}` paths from the first iteration.
- `.redocly.lint-ignore.yaml`: ignore one `no-ambiguous-paths` warning between the new path and `/customers/external-accounts/{externalAccountId}` (theoretical-only; runtime routing is unambiguous since externalAccountIds are prefixed Grid IDs).
**Stainless config (`.stainless/stainless.yml`)**
- Added `customers.create_kyc_link: post /customers/{customerId}/kyc-link` method.
- Registered `KycLinkResponse` and `KycProvider` in `customers.models`.
- Removed the iteration-1 `kyc_links` subresource and stale `get_kyc_link` mapping.
**CLI (`cli/src/commands/customers.ts`)**
- Replaced the broken `kyc-link` command (which previously POSTed to a GET endpoint) with `customers kyc-link <customerId> [--redirect-uri ...]`.
**Docs (`mintlify/`)**
- `snippets/kyc/kyc-unregulated.mdx`, `snippets/creating-customers/customers.mdx`, `global-p2p/onboarding/configuring-customers.mdx`, `payouts-and-b2b/quickstart.mdx` rewritten around the two-step flow (create customer → generate link).
## Open follow-ups (not in this PR)
- The Stainless config snapshot in `.stainless/stainless.yml` reflects the new shape, but the upstream Stainless config (on app.stainless.com) still references the old endpoint and must be updated via the Stainless studio for the SDK preview build to succeed. Studio link: https://app.stainless.com/lightspark/grid/studio?language=openapi
- Reconcile `KycStatus` / `KybStatus` with the documented webhook outcomes (`EXPIRED`, `CANCELED` are missing from the customer enum today).
## Test plan
- [ ] `make build` passes (verified locally)
- [ ] `make lint-openapi` Redocly validation passes (verified locally; spectral missing is a local env issue)
- [ ] CLI type-checks (verified locally)
- [ ] Stainless config updated via studio so the preview SDK build is green
- [ ] Spec change reviewed for backend feasibility
- [ ] Mintlify preview renders the new endpoint correctly
ebf457f to
28cffbf
Compare
|
@greptile review |

Summary
Replaces
GET /customers/kyc-linkwithPOST /customers/{customerId}/kyc-link— a nested action on an existing customer. Customer creation and KYC link generation are now two separate steps.Motivation
Previous iterations of this PR explored two designs:
GET /customers/kyc-link(current production) — wrong verb (it creates resources), requiredplatformCustomerIdoverloaded as both customer-uniqueness key and idempotency key, no way to shape the hosted flow, no way to check status without webhooks.POST /customers/kyc-linksreturning akycLinkIdresource (first iteration) — better, but conflated customer creation with link generation. The request body had to duplicate every field on the customer schema (customerType,region,currencies,email, etc.).After reviewer feedback, this PR lands on the cleanest shape: create the customer first with
POST /customers, then callPOST /customers/{customerId}/kyc-linkto generate a link for that customer. The request body becomes trivial (just optionalredirectUri), the response is minimal, and there's no addressablekycLinkIdresource to manage. Status is read off the customer's existingkycStatusfield; the webhook contract is unchanged.Endpoint shape
Each call returns a fresh single-use URL. Previously-issued links remain single-use but aren't invalidated — the platform can call again any time the customer needs a new link.
providerexposes the KYC provider Grid is using behind the scenes; the optionaltokenlets platforms embed the provider's SDK directly (e.g. Sumsub Web SDK) instead of redirecting to the hosted URL. Both paths update the samekycStatuson the customer.Changes
OpenAPI (
openapi/)POST /customers/{customerId}/kyc-link(createCustomerKycLink).KycLinkCreateRequest(one optional field,redirectUri),KycLinkResponse(4 fields,required: [kycUrl, expiresAt, provider]),KycProviderenum (SUMSUB).KycLinkStatus, the oldGET /customers/kyc-linkpath, and the intermediatePOST /customers/kyc-links+GET /customers/kyc-links/{kycLinkId}paths from the first iteration..redocly.lint-ignore.yaml: ignore oneno-ambiguous-pathswarning between the new path and/customers/external-accounts/{externalAccountId}(theoretical-only; runtime routing is unambiguous since externalAccountIds are prefixed Grid IDs).Stainless config (
.stainless/stainless.yml)customers.create_kyc_link: post /customers/{customerId}/kyc-linkmethod withbody_param_name: KycLinkCreateRequest.KycLinkCreateRequest,KycLinkResponse, andKycProviderincustomers.models.kyc_linkssubresource and staleget_kyc_linkmapping.CLI (
cli/src/commands/customers.ts)kyc-linkcommand (which previously POSTed to a GET endpoint) withcustomers kyc-link <customerId> [--redirect-uri ...].Docs (
mintlify/)snippets/kyc/kyc-unregulated.mdx,snippets/creating-customers/customers.mdx,global-p2p/onboarding/configuring-customers.mdx,payouts-and-b2b/quickstart.mdxrewritten around the two-step flow (create customer → generate link).Open follow-ups (not in this PR)
.stainless/stainless.ymlreflects the new shape, but the upstream Stainless config (on app.stainless.com) still references the old endpoint and must be updated via the Stainless studio for the SDK preview build to succeed. Studio link: https://app.stainless.com/lightspark/grid/studio?language=openapiKycStatus/KybStatuswith the documented webhook outcomes (EXPIRED,CANCELEDare missing from the customer enum today).Test plan
make buildpasses (verified locally)make lint-openapiRedocly validation passes (verified locally)spectral lintreports 0 errors (verified locally vianpx)