Skip to content

Grid Programmatic KYC API#726

Open
akanter wants to merge 5 commits into
mainfrom
07-22-kyc-singleton-implementation-plan
Open

Grid Programmatic KYC API#726
akanter wants to merge 5 commits into
mainfrom
07-22-kyc-singleton-implementation-plan

Conversation

@akanter

@akanter akanter commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 grid-api slice of the KYC singleton flow — OpenAPI schema additions, the new EDD endpoint, and Mintlify sandbox docs.

OpenAPI schemas

  • taxIdentification added to IndividualCustomerFields.yaml as a single optional object referencing the existing IdentificationType enum (SSN | ITIN | EIN | NON_US_TAX_ID).
  • New TaxIdentification.yaml (idType, identifier, countryOfIssuance).
  • New shared FieldConstraint.yaml under errors/ — regex / format / enum / min-max length. Error400.yaml's details description documents the convention: on submit-endpoint validation failures, details.constraint conforms to FieldConstraint, giving platforms a machine-readable validator hint they can use to pre-validate their onboarding form before re-submitting. Applies to POST /customers, PATCH /customers/{id}, POST /customers/edd, PATCH /customers/edd.
  • New enum schemas: IndividualSourceOfFundsCategory (11 individual-appropriate values), SourceOfWealthCategory, AnnualIncomeRange, NetWorthRange, PepStatus, ExpectedMonthlyTransactionCount, ExpectedMonthlyTransactionVolume. IndividualSourceOfFundsCategory sits alongside the existing (business) SourceOfFundsCategory, keeping the business flow's enum untouched. ExpectedMonthlyTransactionCount/Volume are extracted from previously-inline definitions in BusinessInfo.yaml and shared with the new EDD schema.

New EDD endpoint

Flat linked-resource pattern per openapi/README.md — customerId in the request body on POST, as a query param on GET/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).
  • Registered in 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).
  • Individual KYC outcome table: fullName suffix 001 → PENDING, 002 → REJECTED, 003 → APPROVED with EDD-required flow, else APPROVED.
  • Full curl example: create individual customer → verify (returns VerificationError with EDD_INFORMATION_MISSING) → POST /customers/edd → retry verification → APPROVED.
  • Import in mintlify/api-reference/sandbox-testing.mdx points at the renamed snippet.

Verification

  • make build succeeds — bundled openapi.yaml and mintlify/openapi.yaml regenerated.
  • make lint passes (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.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 24, 2026 8:30pm
grid-wallet-demo Ignored Ignored Preview Jul 24, 2026 8:30pm

Request Review

akanter commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

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

@ls-bolt ls-bolt Bot mentioned this pull request Jul 22, 2026
@ls-bolt

ls-bolt Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

@ls-bolt

ls-bolt Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

@ls-bolt

ls-bolt Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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.

@ls-bolt

ls-bolt Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

⚡ 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:

  1. Trim the plan doc here — strip the LexisNexis / OFAC content (per your note that OFAC is a separate thing), align phasing with what's in plan-kyc-api-design #700's Implementation phasing section.
  2. Land the Phase 1 grid-api items in this PR: taxIdentification[] on IndividualCustomerFields, optional EDD extras, VerificationError code + constraint, new GET /verification/requirements endpoint + response schema, submitForVerification flag on POST /customers / PATCH /customers/{id}, and rename sandbox-kyb-verification.mdxsandbox-verification.mdx with the new individual-customer KYC subsection.
  3. Regenerate the bundled openapi.yaml via make build and run make lint.
  4. Sparkcore backend work is a separate PR in webdev/sparkcore — will handle after this lands.

@ls-bolt
ls-bolt Bot force-pushed the 07-22-kyc-singleton-implementation-plan branch from 8f5c9ef to d64ecbf Compare July 22, 2026 23:13
@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Jul 22, 2026
@ls-bolt

ls-bolt Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

First Phase 1 slice pushed (R2). make lint passes; bundled openapi.yaml regenerates cleanly.

In this push:

  • Plan doc rewrite — stripped all LexisNexis / OFAC content, tightened to Phase 1 grid-api scope, pointing to design PR plan-kyc-api-design #700 for full context.
  • §1 taxIdentification[] on IndividualCustomerFields — new TaxIdentification.yaml schema (idType + identifier + countryOfIssuance), array on the individual customer.
  • §1a Optional EDD extras on IndividualCustomerFields — added sourceOfFundsCategories (reuses existing), sourceOfWealthCategories (new enum), purposeOfAccount (reuses existing), expectedMonthlyTransactionCount / Volume (extracted to shared enum schemas), annualIncomeRange (new), netWorthRange (new), pepStatus (new).
  • §2 VerificationError code + constraint — added optional fields; FieldConstraint.yaml extracted as shared schema (will be reused by /verification/requirements in the next slice).
  • §5 submitForVerification on POST /customers — new optional boolean on the create-request; new CustomerCreateResponse.yaml wraps { customer, verification? }; the 201 response schema on POST /customers now uses the wrapped shape.

Still to come in follow-up commits on this PR:

  • §5 submitForVerification on PATCH /customers/{customerId} (mirrors POST — add flag to CustomerUpdateRequest, wrap the 200 response).
  • §3 GET /verification/requirements endpoint spec + VerificationRequirements.yaml response schema + supporting FieldRequirement, DocumentRequirement, AddressFormat schemas + registration in root openapi.yaml.
  • Mintlify — rename sandbox-kyb-verification.mdxsandbox-verification.mdx, add "Individual customer verification" subsection with the fullName-suffix table, update imports across api-reference/sandbox-testing.mdx and per-product pages.
  • One small design decision surfaced: the existing SourceOfFundsCategory enum is business-focused (OPERATING_REVENUE, INVESTMENT_INCOME, VENTURE_CAPITAL, etc.) — the individual example I put in the schema had to fall back to PERSONAL_SAVINGS. Worth deciding whether we expand the existing enum with individual-appropriate values (SALARY, FREELANCE_INCOME, etc.) or add a parallel IndividualSourceOfFundsCategory. Not urgent — schema shape is fine as-is — but flagging for later.

Sparkcore backend PR will follow separately (different repo).

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

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

cli

feat: plan-kyc-singleton-implementation

go

feat(api): add EDD fields to customers, verification error constraints, CustomerNewResponse

kotlin

feat: plan-kyc-singleton-implementation

openapi

feat: plan-kyc-singleton-implementation

php

feat: plan-kyc-singleton-implementation

python

feat(api): add CustomerCreateResponse model, customer verification/tax/PEP fields

ruby

feat: plan-kyc-singleton-implementation

typescript

feat: plan-kyc-singleton-implementation

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

grid-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/50a2ff276c35e64b870f8bfca61c5d2ec239ab73/dist.tar.gz
New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️

New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-ruby studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@ae178b383840e1fa5416eb76863439a3bb6a2e3a
New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-kotlin studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/04e21396a9b9f06559315332da2ae31ae8423d0f/grid-0.0.1-py3-none-any.whl
New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-php studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅lint ✅test ✅

New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`
grid-cli studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗

New diagnostics (3 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/{customerId}/edd`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `patch /customers/{customerId}/edd`

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-07-24 20:34:12 UTC

@mintlify

mintlify Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Jul 22, 2026, 11:14 PM

@ls-bolt
ls-bolt Bot force-pushed the 07-22-kyc-singleton-implementation-plan branch from d64ecbf to 67a3a37 Compare July 22, 2026 23:15
@ls-bolt
ls-bolt Bot force-pushed the 07-22-kyc-singleton-implementation-plan branch from 67a3a37 to 0783bc0 Compare July 23, 2026 01:20
@ls-bolt

ls-bolt Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

R4 pushed — aligned with the 6 design changes in #700 R13. make lint clean.

Reverted from R3:

  • Removed submitForVerification from CustomerCreateRequest.yaml; POST /customers 201 response reverted to CustomerOneOf (deleted CustomerCreateResponse.yaml).
  • Removed code from VerificationError.yaml (kept constraint — the existing VerificationErrorType enum already covers programmatic categorization).
  • Removed all EDD-extras fields from IndividualCustomerFields.yaml; they moved to the new EDD endpoint below.
  • Changed taxIdentification on IndividualCustomerFields.yaml from array to single object (dropped the multi-jurisdictional-taxpayer array).

Added:

  • New POST /customers/{customerId}/edd, GET, PATCH endpoint (openapi/paths/customers/customers_{customerId}_edd.yaml).
  • New CustomerEdd.yaml request/response schema — 8 EDD fields referencing existing / new enum schemas (SourceOfWealthCategory, AnnualIncomeRange, NetWorthRange, PepStatus, SourceOfFundsCategory, PurposeOfAccount, ExpectedMonthlyTransactionCount/Volume).
  • Registered /customers/{customerId}/edd in openapi/openapi.yaml.
  • Extended mintlify/snippets/sandbox-verification.mdx — new 003 suffix row on the individual-customer KYC outcome table + a full curl example flow showing create → verify (MISSING_FIELD on edd.*) → POST /customers/{id}/edd → retry.
  • Plan doc (docs/plans/2026-07-22-kyc-singleton-implementation.md) rewritten to match the new scope.

Kept from R3:

  • Enum schemas (SourceOfWealthCategory.yaml, AnnualIncomeRange.yaml, NetWorthRange.yaml, PepStatus.yaml, ExpectedMonthlyTransactionCount.yaml, ExpectedMonthlyTransactionVolume.yaml) — now referenced by CustomerEdd.yaml instead of IndividualCustomerFields.yaml.
  • TaxIdentification.yaml — now referenced as a single object rather than array element.
  • FieldConstraint.yaml — still referenced by VerificationError.constraint.
  • sandbox-verification.mdx rename (from sandbox-kyb-verification.mdx) + <SandboxVerification /> import update in mintlify/api-reference/sandbox-testing.mdx.

One design nit flagged in the plan doc's Risks section: existing SourceOfFundsCategory enum is business-focused (OPERATING_REVENUE, INVESTMENT_INCOME, VENTURE_CAPITAL, etc.). Reusing it for individual EDD leaves only PERSONAL_SAVINGS and OTHER clearly applicable. Worth deciding whether to expand it with individual-appropriate values (SALARY, FREELANCE_INCOME) or add a parallel IndividualSourceOfFundsCategory enum. Not blocking Phase 1 — schema is functionally fine — but flagging as a design decision.

Comment thread docs/plans/2026-07-22-kyc-singleton-implementation.md Outdated
Comment thread openapi/paths/customers/customers_{customerId}_edd.yaml
@akanter akanter added the bolt label Jul 23, 2026 — with Graphite App
@akanter
akanter requested a review from wuvictor-95 July 24, 2026 04:57
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces Phase 1 of the Grid programmatic KYC API: a new /customers/edd resource (GET/POST/PATCH) for Enhanced Due Diligence fields on individual customers, taxIdentification on IndividualCustomerFields, a FieldConstraint schema for machine-readable validation hints in 400 error responses, and updated Mintlify sandbox docs combining KYB and individual KYC (including the EDD flow) into a single snippet.

  • New CustomerEdd and CustomerEddCreateRequest schemas carry 11 optional EDD fields (source of funds/wealth categories, purpose of account, income/net-worth ranges, and PEP status); the flat linked-resource pattern is followed consistently with customerId in the POST body and as a query param for GET/PATCH.
  • Seven new enum schemas (IndividualSourceOfFundsCategory, SourceOfWealthCategory, AnnualIncomeRange, NetWorthRange, PepStatus, ExpectedMonthlyTransactionCount, ExpectedMonthlyTransactionVolume) are cleanly separated from the existing business-customer enums.
  • The FieldConstraint.yaml schema is added under errors/ to describe validation hint objects surfaced in Error400.details.constraint, but it is never $ref'd in the schema tree and is therefore absent from the bundled output — docs and SDK generators won't see it.

Confidence Score: 4/5

Safe to merge with one follow-up needed: FieldConstraint exists in source but won't appear in docs or generated SDKs until it is formally $ref'd.

The EDD endpoint design, enum schemas, and sandbox docs are all well-structured. The main gap is that FieldConstraint.yaml is created but never wired into the schema tree via $ref, so it is silently dropped by the Redocly bundler and never surfaces in components.schemas of the published spec — the contract it documents (details.constraint) stays typed as a free-form object from both a documentation and SDK-generation perspective. This is a real but contained gap that doesn't break any existing functionality; the rest of the change is low-risk.

openapi/components/schemas/errors/FieldConstraint.yaml (orphaned schema, not bundled); openapi/components/schemas/customers/CustomerEddCreateRequest.yaml (field duplication from CustomerEdd)

Important Files Changed

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)
Loading
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

Comment thread openapi/components/schemas/errors/FieldConstraint.yaml
Comment thread openapi/components/schemas/customers/CustomerEddCreateRequest.yaml Outdated
Comment on lines +1 to +5
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.

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.

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

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@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 =) )

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.

Yeah I had the same thought, but tbh I don't feel super strongly here, so I'm cool with you going either direction. It's something we're not super consistent with right now :-/. @shreyav or @pengying may have a stronger opinion though.

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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.

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-accounts etc.
  • /customers/{customerId}/edd — sub-resource pattern, consistent with /customers/{customerId}/kyc-link etc.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(The transaction EDD has different fields though)

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.

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}/eddCustomerEdd
  • /transactions/{transactionId}/eddTransactionEdd (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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If we really need to query for all transaction EDD records for a customer I think that might be a separate thing

@ls-bolt
ls-bolt Bot force-pushed the 07-22-kyc-singleton-implementation-plan branch from 9c8bb1e to ae8a317 Compare July 24, 2026 05:07
@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision — Greptile P1 + 2×P2 fixes

Applied all three fixes from the Greptile review:

P1 — FieldConstraint was orphaned (thread) — Error400.yaml's details now declares typed field + constraint properties (with $ref to FieldConstraint.yaml), keeping additionalProperties: true for varied error shapes. Bundle now surfaces FieldConstraint under components.schemas.

P2 — CustomerEddCreateRequest duplication (thread) — refactored to allOf composition ($ref CustomerEdd.yaml + customerId in a small object). No more field duplication.

P2 — Route ordering (thread) — /customers/edd now registered before /customers/{customerId} in openapi/openapi.yaml (most-specific-first).

make build regenerated the bundle; make lint clean (exit 0, no new errors).

@jklein24 jklein24 left a comment

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.

Generally LGTM! Just minor comments/questions

Comment thread openapi/components/schemas/errors/FieldConstraint.yaml
Comment thread openapi/components/schemas/errors/FieldConstraint.yaml Outdated
Comment thread mintlify/snippets/sandbox-verification.mdx
@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision — @jklein24's FieldConstraint review

Documentation-only. Expanded the field descriptions to formalize the answers I gave inline:

  • Top-level: FieldConstraint is now explicitly documented as driving both (a) client-side pre-validation and (b) direct form-UI rendering (HTML5 <input type> / inputmode / autocomplete / maxlength).
  • format: usage guidance added — prefer HTML5 input-type names when applicable, use semantic slugs otherwise, treat unknown values as opaque. Example updated to email (HTML5 canonical).
  • pattern: notes that length constraints are surfaced separately so consumers can wire native controls without evaluating a regex.
  • minLength / maxLength: rationale for being distinct fields (native <input maxlength> wire-up) documented.

No schema shape change. make build regenerated the bundle; make lint clean.

Sandbox-defaults thread (mdx line 5) noted as explicit non-goal → follow-up ticket.

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]>
Comment on lines +1 to +5
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@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 =) )

Comment thread openapi/components/schemas/errors/FieldConstraint.yaml Outdated
Comment thread openapi/components/schemas/errors/FieldConstraint.yaml Outdated
@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision — @akanter trim on FieldConstraint descriptions

23222e8c — dropped the rationale text I added in 65ba025b that was justifying design choices (why min/max are separate from pattern; HTML5-vs-semantic conventions on format; contract statements at the top level). Descriptions now say what each field IS.

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.

@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

📌 Bolt Status

2026-07-24 17:54:14 UTC — ⚡ Agent staking-corona-11(#11) started


Feedback

netWorthRange:
$ref: ./NetWorthRange.yaml
pepStatus:
$ref: ./PepStatus.yaml

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.

created at timestamp

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.

@-

example: US
address:
$ref: ../common/Address.yaml
taxIdentification:

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.

i thought you were going to make this an array?

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.

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?

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.

@-

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.

@-

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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!

@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision 11

Addressed @pengying's two inline comments:

  • CustomerEdd.yaml:52 — added createdAt + updatedAt (both readOnly: true, ISO-8601), matching the Customer.yaml timestamp convention. Added updatedAt as well since EDD is PATCH-able.
  • IndividualCustomerFields.yaml:26 — reverted taxIdentification from single $ref to array. My 07-23 R14 note on #700 said we could revisit if real demand showed up; dual citizens / multi-jurisdictional taxpayers are common enough to make the array the safer default.

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 gt modify pick it up):

  • errors/FieldConstraint.yaml — trimmed the rationale prose per @akanter's earlier feedback ("description says WHAT the field is, not WHY it exists"). Net: 45 lines → 35 lines.

Rebundled openapi.yaml via make build; make lint exits 0 (only pre-existing informational warnings on other schemas).

…xIdentifier/taxIdCountryOfIssuance (@akanter direction, Stripe-style)
@ls-bolt
ls-bolt Bot force-pushed the 07-22-kyc-singleton-implementation-plan branch from f0a2dd8 to 1575a8b Compare July 24, 2026 19:56
@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision 12

Flattened taxIdentification per @akanter's design landing on the offline discussion. Reverted R11's array change and eliminated the TaxIdentification wrapper entirely — the three fields now live at top-level on IndividualCustomerFields with a taxId prefix:

  • taxIdType$ref: ./IdentificationType.yaml
  • taxIdentifier — the tax-identification number (SSN / ITIN / foreign tax ID)
  • taxIdCountryOfIssuance — ISO 3166-1 alpha-2 country code, required when taxIdType is NON_US_TAX_ID

Follow-on cleanup:

  • Deleted openapi/components/schemas/customers/TaxIdentification.yaml (no longer referenced)
  • Updated Error400.details.field example taxIdentification.identifiertaxIdentifier
  • Rebundled openapi.yaml + mintlify/openapi.yaml

Rationale (matches Stripe's Person.id_number / id_number_type shape): 90% case for Grid customers is one primary tax ID; wrapping in a sub-object was dead weight for that common path. If real dual-citizen / multi-jurisdictional demand shows up post-launch, we can add additionalTaxIdentifications: [] alongside the primary flat fields — additive, no breaking change.

Downstream impact on webdev#31108 (PR B)

Sparkcore-side already has flat columns on the ent (tax_id_type, tax_identifier, tax_id_country_of_issuance) from the earlier column-rename, so the DB shape is already correct. The Pydantic-side TaxIdentification sub-object schema will need to flatten to match the API surface. Small handler-wiring change too. Will explore + push as a small PR B revision — flagging separately here in case anyone wants a different approach.

akanter added 2 commits July 24, 2026 20:29
…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.
@ls-bolt

ls-bolt Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Revision 13 — pathParam nesting per @akanter's landing

Switched /customers/edd?customerId={id} (queryParam) to /customers/{customerId}/edd (pathParam nesting), per your call rolling with the RESTful pathParam approach that @shreyav and @jklein24 leaned toward.

Changes:

Composes naturally to /transactions/{transactionId}/edd if we ever ship transaction-EDD (per your "we'll have a matching /transactions/{id}/edd" note).

Sparkcore-side follow-up (webdev #31107): switching the handler routes from /customers/edd (queryParam) to /customers/<customer_id>/edd (pathParam) coming next — will restack #31110 and #31111 onto the new PR A tip.

make build + make lint: clean. Both commits pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants