Grid Programmatic KYC API#726
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
⚡ On it — stripping the LexisNexis content from the plan (that was just answering your investigation question, not implementation scope). Also adding sandbox support to the design doc (#700) since customers need to be able to drive the KYC flow through sandbox. |
|
Got it — leaving this plan PR alone for now. Jumping over to grid-api#700 to add the sandbox section to the design doc first. Once the sandbox design is settled there, I'll come back and update this plan (both dropping the LexisNexis content per your earlier note, and incorporating sandbox into the implementation phasing). |
|
Aligned on the strategy. Reworking Phase 1 to be: full API surface (schemas + new endpoints) + full sandbox behavior, no real production wire-up yet. That lets customers integrate against sandbox immediately while we progressively fill in the real Sumsub / LexisNexis / doc pass-through behind the same API. Production wire-up becomes Phase 2+. Also dropping the LexisNexis content per your earlier note. Standby. |
|
⚡ Starting Phase 1 implementation now. Since design doc #700 has the current-state design (with sandbox first-class and OFAC treated as separate work), I'll:
|
8f5c9ef to
d64ecbf
Compare
|
First Phase 1 slice pushed (R2). In this push:
Still to come in follow-up commits on this PR:
Sparkcore backend PR will follow separately (different repo). |
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ grid-typescript studio · code · diff
✅ grid-openapi studio · code · diff
✅ grid-ruby studio · code · diff
✅ grid-go studio · code · diff
✅ grid-kotlin studio · code · diff
✅ grid-python studio · code · diff
✅ grid-php studio · code · diff
✅ grid-cli studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
d64ecbf to
67a3a37
Compare
67a3a37 to
0783bc0
Compare
|
R4 pushed — aligned with the 6 design changes in #700 R13. Reverted from R3:
Added:
Kept from R3:
One design nit flagged in the plan doc's Risks section: existing |
84dcfb0 to
a39f570
Compare
a39f570 to
bd115b5
Compare
bd115b5 to
65b67eb
Compare
65b67eb to
9f76353
Compare
Greptile SummaryThis PR introduces Phase 1 of the Grid programmatic KYC API: a new
Confidence Score: 4/5Safe to merge with one follow-up needed: The EDD endpoint design, enum schemas, and sandbox docs are all well-structured. The main gap is that openapi/components/schemas/errors/FieldConstraint.yaml (orphaned schema, not bundled); openapi/components/schemas/customers/CustomerEddCreateRequest.yaml (field duplication from CustomerEdd)
|
| Filename | Overview |
|---|---|
| openapi/components/schemas/errors/FieldConstraint.yaml | New schema for machine-readable field validation hints, but never $ref'd anywhere — excluded from the bundled output and invisible to docs/SDKs. |
| openapi/paths/customers/customers_edd.yaml | New GET/POST/PATCH endpoints for EDD; well-structured per the flat linked-resource pattern; static route registered after the parameterized /customers/{customerId} route in openapi.yaml. |
| openapi/components/schemas/customers/CustomerEdd.yaml | New EDD response/PATCH body schema with 11 optional fields; clean design, well-documented with FLOW/STOCK distinctions for source-of-funds/wealth. |
| openapi/components/schemas/customers/CustomerEddCreateRequest.yaml | POST request body schema; duplicates all 11 EDD field definitions from CustomerEdd verbatim rather than composing via allOf, creating a maintenance risk. |
| openapi/components/schemas/customers/TaxIdentification.yaml | New TaxIdentification schema added to IndividualCustomerFields; correctly marks idType and identifier as required, with countryOfIssuance documented as conditionally required for NON_US_TAX_ID. |
| openapi/components/schemas/errors/Error400.yaml | Expanded details description to document field and constraint sub-fields; FieldConstraint is referenced only in prose, not via $ref, so it remains an untyped freeform object. |
| mintlify/snippets/sandbox-verification.mdx | New combined sandbox verification snippet covering business KYB, beneficial owner KYC, and individual customer KYC (with EDD flow); replaces deleted sandbox-kyb-verification.mdx. |
| openapi/openapi.yaml | Registers /customers/edd path; placement after /customers/{customerId} is a minor ordering concern but doesn't affect spec correctness. |
Sequence Diagram
sequenceDiagram
participant P as Platform
participant G as Grid API
P->>G: POST /customers (customerType: INDIVIDUAL, fullName: "Jane Doe 003")
G-->>P: "200 Customer {id, kycStatus: PENDING}"
P->>G: "POST /verifications {customerId}"
G-->>P: "200 {verificationStatus: RESOLVE_ERRORS, errors: [{code: MISSING_FIELD, field: "edd.*"}]}"
P->>G: "POST /customers/edd {customerId, sourceOfWealthCategories, purposeOfAccount, annualIncomeRange}"
G-->>P: 200 CustomerEdd
P->>G: "POST /verifications {customerId}"
G-->>P: "200 {verificationStatus: IN_PROGRESS, kycStatus: APPROVED}"
Note over P,G: Optional: retrieve or partially update EDD later
P->>G: "GET /customers/edd?customerId={id}"
G-->>P: 200 CustomerEdd
P->>G: "PATCH /customers/edd?customerId={id} {pepStatus: NONE}"
G-->>P: 200 CustomerEdd (merged)
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
openapi/components/schemas/errors/FieldConstraint.yaml:1-33
**`FieldConstraint` schema is unreachable in the bundled output**
`FieldConstraint.yaml` is created as a new source schema, but nothing in the spec ever `$ref`'s it. Redocly's bundler only includes schemas that are reachable via `$ref` from the root, so `FieldConstraint` never appears in `components.schemas` of the generated `openapi.yaml` or `mintlify/openapi.yaml` (confirmed: a grep for `$ref.*FieldConstraint` across the whole repo returns zero hits). The `details.constraint` property in `Error400.yaml` is only described in prose — the schema remains typed as a free-form `additionalProperties: true` object. This means SDK generators won't emit a `FieldConstraint` type, and API docs won't render it as a linked schema. To surface it formally, `Error400.yaml`'s `details` property needs to either `$ref` it directly (e.g. via a typed wrapper or discriminated details schema) or the schema needs to be registered in the `openapi/openapi.yaml` components section so the bundler picks it up.
### Issue 2 of 3
openapi/components/schemas/customers/CustomerEddCreateRequest.yaml:1-56
**`CustomerEddCreateRequest` duplicates all `CustomerEdd` fields verbatim**
All 11 EDD field definitions (`sourceOfFundsCategories`, `sourceOfWealthCategories`, `purposeOfAccount`, and their `Other`-description companions, plus the 4 range/status enums) are copied word-for-word from `CustomerEdd.yaml`. Every future field addition to `CustomerEdd` will require a parallel manual update here. Using `allOf` composition — `allOf: [{$ref: ./CustomerEdd.yaml}]` plus a `customerId` property in `CustomerEddCreateRequest` — would keep both in sync automatically and match the DRY pattern used elsewhere in this spec (e.g. `IndividualCustomer` composing `IndividualCustomerFields`).
### Issue 3 of 3
openapi/paths/customers/customers_edd.yaml:1-5
**Static route registered after parameterized route in `openapi/openapi.yaml`**
In `openapi/openapi.yaml`, `/customers/edd` is placed after `/customers/{customerId}`. While OpenAPI itself doesn't mandate ordering, many server frameworks and generated client SDKs resolve paths top-down, and the string `"edd"` is a valid `customerId` value — so a misconfigured router could match `GET /customers/edd` against the `/customers/{customerId}` handler with `customerId="edd"`. The backend impl in webdev#31010 presumably handles this correctly, but registering `/customers/edd` before `/customers/{customerId}` in the YAML is the conventional defensive posture ("most specific first") and would be the safer documentation convention.
Reviews (1): Last reviewed commit: "plan-kyc-singleton-implementation" | Re-trigger Greptile
| get: | ||
| summary: Get a customer's enhanced-due-diligence (EDD) fields | ||
| description: >- | ||
| Retrieve the EDD fields previously supplied for a customer. Returns `404` | ||
| if no EDD data has been recorded for the customer yet. |
There was a problem hiding this comment.
Static route registered after parameterized route in
openapi/openapi.yaml
In openapi/openapi.yaml, /customers/edd is placed after /customers/{customerId}. While OpenAPI itself doesn't mandate ordering, many server frameworks and generated client SDKs resolve paths top-down, and the string "edd" is a valid customerId value — so a misconfigured router could match GET /customers/edd against the /customers/{customerId} handler with customerId="edd". The backend impl in webdev#31010 presumably handles this correctly, but registering /customers/edd before /customers/{customerId} in the YAML is the conventional defensive posture ("most specific first") and would be the safer documentation convention.
Knowledge Base Used: Customers and KYC
Prompt To Fix With AI
This is a comment left during a code review.
Path: openapi/paths/customers/customers_edd.yaml
Line: 1-5
Comment:
**Static route registered after parameterized route in `openapi/openapi.yaml`**
In `openapi/openapi.yaml`, `/customers/edd` is placed after `/customers/{customerId}`. While OpenAPI itself doesn't mandate ordering, many server frameworks and generated client SDKs resolve paths top-down, and the string `"edd"` is a valid `customerId` value — so a misconfigured router could match `GET /customers/edd` against the `/customers/{customerId}` handler with `customerId="edd"`. The backend impl in webdev#31010 presumably handles this correctly, but registering `/customers/edd` before `/customers/{customerId}` in the YAML is the conventional defensive posture ("most specific first") and would be the safer documentation convention.
**Knowledge Base Used:** [Customers and KYC](https://app.greptile.com/lightspark/-/custom-context/knowledge-base/lightsparkdev/grid-api/-/docs/customers-and-kyc.md)
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed. Reordered openapi/openapi.yaml so /customers/edd is registered before /customers/{customerId} (most-specific-first). Bundled output confirms — /customers/edd at line 597, /customers/{customerId} at line 741. Matches the conventional defensive posture.
There was a problem hiding this comment.
@pengying, @jklein24 this is actually a little bit funky that we have /customers/{id} and /customers/edd?customerId={id}. Should we go back to just having /customers/{id}/edd ? I recognize that the queryParam route is more aligned with our best practices, but the pathParam route is more consistent with what we have.
(not a question for bolt, asking the humans for opinions here =) )
There was a problem hiding this comment.
All of the other endpoints use query params.
In the past I've seen EDD requested multiple times for a customer and for transactions. I'm a bit uncertain on exactly how this will be expanded upon past KYC/KYB but maybe useful to keep it flexible.
could also be at /edd
There was a problem hiding this comment.
Would we need new EDD records or just to update the existing one? I also want to clarify that EDD for a transaction is different and we will definitely get multiple of those across multiple transactions.
My sense is that we will only need a single EDD record per customer, or a single per transaction.
There was a problem hiding this comment.
That clarification resolves the ambiguity that was driving the /edd top-level proposal. If it's one EDD record per customer (updated in place) and one per transaction (separate concept), the current PR's upsert/singleton model is correct — POST returning 200 with create-or-replace semantics and GET returning a single object are both appropriate.
The top-level /edd resource was the right answer if multiple EDD records could accumulate per customer. Since they don't, it's unnecessary complexity.
For routing, this brings it back to the simpler binary:
/customers/edd?customerId=— flat query-param pattern, consistent with/customers/internal-accountsetc./customers/{customerId}/edd— sub-resource pattern, consistent with/customers/{customerId}/kyc-linketc.
Either works cleanly for a singleton. Transaction EDD (/transactions/{id}/edd or /edd?transactionId=) can be designed independently when that work is scoped, since it's a separate resource regardless.
The thread's last settled position was /customers/{customerId}/edd (akanter's summary after the team discussion), with pengying's pushback on sub-resourcing still open. Given that EDD is identity/verification-adjacent (not a financial resource like internal accounts), the sub-resource precedent from /customers/{customerId}/kyc-link seems like the closer analog. But this is still a team call — just wanted to confirm the singleton model itself is validated by your clarification.
There was a problem hiding this comment.
(The transaction EDD has different fields though)
There was a problem hiding this comment.
That actually strengthens the case against a shared top-level /edd namespace — if customer EDD and transaction EDD have different schemas, a single /edd collection would need a discriminator or be permanently ambiguous. They're better modeled as separate sub-resources with their own schemas:
/customers/{customerId}/edd→CustomerEdd/transactions/{transactionId}/edd→TransactionEdd(future)
This aligns with the sub-resource precedent from /customers/{customerId}/kyc-link and avoids conflating two distinct schemas under one endpoint. The current PR's CustomerEdd.yaml stays as-is; TransactionEdd would be a separate schema file when that work is scoped.
So the thread's last settled position — /customers/{customerId}/edd — still holds, and this clarification removes the main argument for deviating from it.
There was a problem hiding this comment.
If we really need to query for all transaction EDD records for a customer I think that might be a separate thing
9c8bb1e to
ae8a317
Compare
Revision — Greptile P1 + 2×P2 fixesApplied all three fixes from the Greptile review: P1 — P2 — P2 — Route ordering (thread) —
|
jklein24
left a comment
There was a problem hiding this comment.
Generally LGTM! Just minor comments/questions
Revision — @jklein24's
|
Addresses @jklein24's review questions on grid-api#726: - top-level: explicit that FieldConstraint drives both client-side pre-validation and direct form-UI rendering (HTML5 input type, inputmode, autocomplete, maxlength attributes). - format: usage guidance — prefer HTML5 input type names when the field maps to one; use semantic slugs otherwise; treat unknown values as opaque and fall back to accompanying pattern/length constraints. Example changed to `email` (HTML5 canonical). - pattern: notes that length is surfaced as separate minLength/ maxLength fields so consumers can wire native controls without regex evaluation. - minLength / maxLength: rationale for being distinct fields (native <input maxlength> wire-up) documented. No schema shape change; description-only. Co-Authored-By: jklein24 <[email protected]>
| get: | ||
| summary: Get a customer's enhanced-due-diligence (EDD) fields | ||
| description: >- | ||
| Retrieve the EDD fields previously supplied for a customer. Returns `404` | ||
| if no EDD data has been recorded for the customer yet. |
There was a problem hiding this comment.
@pengying, @jklein24 this is actually a little bit funky that we have /customers/{id} and /customers/edd?customerId={id}. Should we go back to just having /customers/{id}/edd ? I recognize that the queryParam route is more aligned with our best practices, but the pathParam route is more consistent with what we have.
(not a question for bolt, asking the humans for opinions here =) )
Revision — @akanter trim on FieldConstraint descriptions
File: 56 → 34 lines (roughly back to the pre-expansion baseline, but with a slightly-improved top-level description clarifying consumer intent). Substantive answers to @jklein24's questions still live in the inline thread replies, which is the right home for design rationale. @jklein24's approval stands — no shape change, just a documentation trim. |
|
📌 Bolt Status 2026-07-24 17:54:14 UTC — ⚡ Agent |
| netWorthRange: | ||
| $ref: ./NetWorthRange.yaml | ||
| pepStatus: | ||
| $ref: ./PepStatus.yaml |
| example: US | ||
| address: | ||
| $ref: ../common/Address.yaml | ||
| taxIdentification: |
There was a problem hiding this comment.
i thought you were going to make this an array?
There was a problem hiding this comment.
also what do you want to do for idType & identifier? If this is a single id, I think we leave it as id type & identifier?
There was a problem hiding this comment.
Not sure what bolt is on about but we flattened it out because I think the times we would need this are pretty rare. Should look better now!
23222e8 to
f0a2dd8
Compare
Revision 11Addressed @pengying's two inline comments:
Also rolled into this commit (was pending in the working tree from a prior local session that lost the dedicated commit to a rebase, so I let
Rebundled |
…xIdentifier/taxIdCountryOfIssuance (@akanter direction, Stripe-style)
f0a2dd8 to
1575a8b
Compare
Revision 12Flattened
Follow-on cleanup:
Rationale (matches Stripe's Downstream impact on webdev#31108 (PR B)Sparkcore-side already has flat columns on the ent ( |
…d (pathParam nesting per @akanter direction) Restful pathParam nesting matches shreyav's and jklein24's inclination: - Renamed openapi/paths/customers/customers_edd.yaml → customers_{customerId}_edd.yaml - Added path-level customerId path parameter (inherited by GET/POST/PATCH) - Removed per-operation customerId query parameter - Deleted CustomerEddCreateRequest.yaml (customerId no longer in body; CustomerEdd.yaml is now the POST request body directly) - openapi/openapi.yaml: path key /customers/edd -> /customers/{customerId}/edd, grouped with other /customers/{customerId}/... subresources Composes naturally to /transactions/{id}/edd if we ever ship transaction-EDD.
Revision 13 — pathParam nesting per @akanter's landingSwitched Changes:
Composes naturally to Sparkcore-side follow-up (webdev #31107): switching the handler routes from
|

Summary
Phase 1 grid-api slice of the KYC singleton flow — OpenAPI schema additions, the new EDD endpoint, and Mintlify sandbox docs.
OpenAPI schemas
taxIdentificationadded toIndividualCustomerFields.yamlas a single optional object referencing the existingIdentificationTypeenum (SSN|ITIN|EIN|NON_US_TAX_ID).TaxIdentification.yaml(idType, identifier, countryOfIssuance).FieldConstraint.yamlundererrors/— regex / format / enum / min-max length.Error400.yaml'sdetailsdescription documents the convention: on submit-endpoint validation failures,details.constraintconforms toFieldConstraint, giving platforms a machine-readable validator hint they can use to pre-validate their onboarding form before re-submitting. Applies toPOST /customers,PATCH /customers/{id},POST /customers/edd,PATCH /customers/edd.IndividualSourceOfFundsCategory(11 individual-appropriate values),SourceOfWealthCategory,AnnualIncomeRange,NetWorthRange,PepStatus,ExpectedMonthlyTransactionCount,ExpectedMonthlyTransactionVolume.IndividualSourceOfFundsCategorysits alongside the existing (business)SourceOfFundsCategory, keeping the business flow's enum untouched.ExpectedMonthlyTransactionCount/Volumeare extracted from previously-inline definitions inBusinessInfo.yamland shared with the new EDD schema.New EDD endpoint
Flat linked-resource pattern per
openapi/README.md— customerId in the request body onPOST, as a query param onGET/PATCH.POST /customers/edd— create or replace a customer's enhanced-due-diligence fields (200, upsert semantics).GET /customers/edd?customerId={id}— retrieve (404 if none recorded).PATCH /customers/edd?customerId={id}— partial update.CustomerEddCreateRequest.yaml— POST body:customerId+ 8 optional EDD fields.CustomerEdd.yaml— response and PATCH body: 8 optional fields (sourceOfFundsCategories,sourceOfWealthCategories,purposeOfAccount,expectedMonthlyTransactionCount,expectedMonthlyTransactionVolume,annualIncomeRange,netWorthRange,pepStatus).openapi/openapi.yaml.Sandbox docs
mintlify/snippets/sandbox-verification.mdx— combined snippet covering all three verification types (business KYB, beneficial-owner KYC, individual-customer KYC).fullNamesuffix001→ PENDING,002→ REJECTED,003→ APPROVED with EDD-required flow, else APPROVED.VerificationErrorwithEDD_INFORMATION_MISSING) →POST /customers/edd→ retry verification → APPROVED.mintlify/api-reference/sandbox-testing.mdxpoints at the renamed snippet.Verification
make buildsucceeds — bundledopenapi.yamlandmintlify/openapi.yamlregenerated.make lintpasses (exit 0; 14 pre-existing warnings, no new errors introduced by this diff).Scope
grid-api (OpenAPI spec + Mintlify docs) only. Sparkcore persistence, sandbox switch wiring, and Sumsub integration ship on webdev#31010 (plan up for approval). OFAC / LexisNexis handling is a separate workstream.
Reply with a comment (e.g. LGTM) to approve — emoji reactions don't notify me here.