Skip to content

refactor(customers): redesign KYC link endpoint as POST /customers/{customerId}/kyc-link#457

Merged
jklein24 merged 1 commit into
mainfrom
05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links
May 12, 2026
Merged

refactor(customers): redesign KYC link endpoint as POST /customers/{customerId}/kyc-link#457
jklein24 merged 1 commit into
mainfrom
05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links

Conversation

@jklein24
Copy link
Copy Markdown
Contributor

@jklein24 jklein24 commented May 11, 2026

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: KycLinkCreateRequest (one optional field, redirectUri), KycLinkResponse (4 fields, required: [kycUrl, expiresAt, provider]), KycProvider enum (SUMSUB).
  • Removed: 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 with body_param_name: KycLinkCreateRequest.
  • Registered KycLinkCreateRequest, 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 lint reports 0 errors (verified locally via npx)
  • CLI type-checks (verified locally)
  • Greptile review: 5/5 confidence, zero unresolved comments
  • 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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview, Comment May 11, 2026 10:32pm

Request Review

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented May 11, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview May 11, 2026, 7:05 AM

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 11, 2026

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

kotlin

feat(api): add kycLinks resource with create/retrieve methods, remove getKycLink

openapi

feat(api): add kyc-links create/retrieve endpoints, remove deprecated kyc-link in customers

python

feat(api): add customers.kyc_links resource, remove customers.get_kyc_link method

typescript

feat(api): add customers.kycLinks resource, remove customers.getKYCLink

Edit this comment to update them. They will appear in their respective SDK's changelogs.

⚠️ grid-kotlin 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 ❗ (prev: test ✅)

New diagnostics (3 error, 1 note)
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.gz
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-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.whl
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}`

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

@jklein24
Copy link
Copy Markdown
Contributor Author

@greptile review this

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR replaces the old GET /customers/kyc-link endpoint with POST /customers/{customerId}/kyc-link, which generates a single-use hosted KYC link for a customer that already exists. The docs, CLI, and SDK config are all updated to match the two-step flow (create customer → generate link).

  • New endpoint POST /customers/{customerId}/kyc-link returns kycUrl, expiresAt, provider, and an optional token for direct SDK embedding; the old endpoint and its implicit customer-creation semantics are removed.
  • Docs rewritten across four Mintlify pages to clarify that POST /customers must be called first, that reaching the redirectUri is not an approval signal, and that status can be tracked via webhook or GET /customers/{customerId}.
  • Idempotency-Key header is supported but a 409 Conflict response is not documented for replays with a mismatched body.

Confidence Score: 5/5

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

Important Files Changed

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
Loading

Fix All in Claude Code

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

Comment thread openapi/components/schemas/customers/KycLinkResponse.yaml
Comment thread openapi/components/schemas/customers/KycLinkResponse.yaml Outdated
Comment thread openapi/paths/customers/customers_kyc_links_{kycLinkId}.yaml Outdated
@jklein24 jklein24 force-pushed the 05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links branch from 3e6dbe1 to 8b43eb1 Compare May 11, 2026 07:12
@jklein24
Copy link
Copy Markdown
Contributor Author

@greptile review

@jklein24 jklein24 force-pushed the 05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links branch from 8b43eb1 to 33dacdf Compare May 11, 2026 07:23
@jklein24 jklein24 marked this pull request as ready for review May 11, 2026 19:34
@jklein24 jklein24 changed the title refactor(customers): redesign KYC link endpoint as POST /customers/kyc-links refactor(customers): redesign KYC link endpoint as POST /customers/{customerId}/kyc-link May 11, 2026
@jklein24 jklein24 force-pushed the 05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links branch from 33dacdf to ebf457f Compare May 11, 2026 22:22
…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
@jklein24 jklein24 force-pushed the 05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links branch from ebf457f to 28cffbf Compare May 11, 2026 22:32
@jklein24
Copy link
Copy Markdown
Contributor Author

@greptile review

@jklein24 jklein24 requested a review from wuvictor-95 May 12, 2026 00:58
@jklein24 jklein24 merged commit b85e654 into main May 12, 2026
10 of 11 checks passed
@jklein24 jklein24 deleted the 05-11-refactor_customers_redesign_kyc_link_endpoint_as_post_/customers/kyc-links branch May 12, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants