Skip to content

Commit 0bd5a28

Browse files
feat(customers): accept typed agreement consents for seven documents (#990)
## Summary - Replaces the single `endUserTermsConsent` field with `agreementConsents`, a list carrying one entry per agreement, so each document's acceptance is recorded and auditable separately. - Adds an `AgreementType` enum with the seven supported documents, and changes `GET /customers/end-user-terms` to return a catalog of all seven as `{ data: [{ type, version, url }] }`. - Spec + docs only. Backend, migration, and generated-client work follow in separate PRs. ## The seven agreement types | Document | `AgreementType` | | --- | --- | | Lightspark Terms of Service (End User Terms) | `LIGHTSPARK_END_USER_TERMS` | | Lightspark E-SIGN consent | `LIGHTSPARK_E_SIGN_CONSENT` | | Lightspark Privacy Policy | `LIGHTSPARK_PRIVACY_POLICY` | | Lead E-SIGN consent | `LEAD_E_SIGN_CONSENT` | | Lead Accountholder Agreement | `LEAD_ACCOUNTHOLDER_AGREEMENT` | | Lead Privacy Policy | `LEAD_PRIVACY_POLICY` | | Lead Cardholder Agreement | `LEAD_CARDHOLDER_AGREEMENT` | Each consent entry carries `type` plus the four existing evidence fields (`acceptedAt`, `ipAddress`, `termsVersion`, `acceptanceMethod`), all required. `termsVersion` is the `version` the catalog returns for that type — versions are scoped per type, so a version valid for one agreement is not valid for another. ## Design decisions worth a reviewer's attention **The endpoint path is unchanged.** `GET /customers/end-user-terms` keeps its path and gains a list response (`operationId` `getEndUserTerms` → `listAgreementDocuments`). Renaming the path would be a second breaking change for no functional gain; happy to rename if you'd prefer to take both at once. **The catalog is not a requirements list.** Seven supported types does not mean every customer must accept all seven. Which documents are required, for which products and customers, is a legal decision this PR deliberately does not encode. **`info.version` is intentionally not bumped.** It tracks the published dated version — a live routing prefix, also hardcoded across the docs and the generated client — not each breaking merge. It has not moved since that rule landed in #679, across 36+ `breaking-change` PRs. The `breaking-change` label and API-reviewer approval are the real gate; flagging it here so that decision is explicit rather than implied. **Clients pinned to the dated version are not affected.** The server change lands this shape on the `/grid/rc` release-candidate surface only, leaving `/grid/2025-10-13` serving the existing contract. Cutting a new dated version is a separate, coordinated release. **`retrieve_end_user_terms` in `.stainless/stainless.yml` is left alone.** It's a published SDK method name and its path is unchanged, so renaming it is an independent SDK-breaking decision. The three model mappings that pointed at now-deleted schemas are updated. ## Migration for existing consumers - Acceptance already recorded under the old field maps only to `LIGHTSPARK_END_USER_TERMS`. Acceptance of one agreement never implies another, so the other six must be collected from each customer. - On update, supplying consents records additional acceptances; evidence already on file for other types is untouched. Omitting the field changes nothing. - `Customer.agreementConsents` is read-only and returns the most recent acceptance per accepted type. It is omitted entirely until the first acceptance, matching how the old `endUserTermsConsent` behaved. ## Changes: 24 files - `openapi/components/schemas/customers/Agreement*.yaml` — 6 new schemas (type, acceptance method, consent request/response, document, list response) - `openapi/components/schemas/customers/EndUserTerms*.yaml` — 3 superseded schemas removed (`EndUserTerms`, `EndUserTermsAcceptanceMethod`, `EndUserTermsConsentRequest`); `EndUserTermsConsent` is kept and marked `deprecated: true` - `openapi/components/schemas/customers/Customer{,CreateRequest,UpdateRequest}.yaml` — field swapped to the list. On `Customer` the field is **optional and omitted when the customer has no acceptances**, rather than always present as `[]` (per review) — matching the deprecated `endUserTermsConsent`, which already documented omit-when-absent - `openapi/paths/customers/customers_agreements.yaml` (new) — `GET /customers/agreements`, the catalog of all seven documents - `openapi/paths/customers/customers_end-user-terms.yaml` — kept and marked `deprecated: true`, still returning its original single-document body so existing callers keep working - `openapi/webhooks/customer-update.yaml` — 2 examples updated to match `Customer` - `openapi/components/schemas/errors/Error400.yaml` — `END_USER_TERMS_VERSION_NOT_FOUND` now fires for any of the seven agreements, so its description no longer names the End User Terms specifically. Widened the wording rather than adding an enum value, which would be a wire change for clients to distinguish cases with the same cause and the same remedy - `.stainless/stainless.yml` — model mappings repointed - `mintlify/` — disclosures rewritten, snippet + changelog entry, regenerated bundles ## Rebase note Rebased onto `main` (6 commits, through #934 "bind one funding source per card"). Two conflicts, both resolved keeping each side: - `Customer.yaml` — main still carried only `endUserTermsConsent`; this branch adds the deprecation marker plus `agreementConsents`. Took this branch's version, which contains both. - `mintlify/changelog.mdx` — both sides added a new top entry. Kept both, main's card-refunds entry above this one. Verified after the rebase that main's card work survived intact (e.g. `CardTransaction.originalTransactionId` is present in the rebuilt bundle) alongside the seven-value enum and the deprecated field. ## Test plan This repo has no test suite; it is spec + docs. Verified: - `make build` bundles cleanly; both generated bundles regenerated and committed. - `make lint` exits 0 with **2 warnings / 14 ignored — byte-identical to the `main` baseline**, confirmed by re-running lint on a stashed tree. No new lint problems. - Parsed the bundle and asserted: the enum holds exactly 7 unique values; all 5 consent fields are required; all three customer schemas expose `agreementConsents` and no longer expose `endUserTermsConsent`; the 200 response resolves to `data[]` of `AgreementDocument` with `{type, version, url}` all required; no `EndUserTerms*` schema remains; `info.version` unchanged. - An automated review pass caught the stale `.stainless` model mappings, which are fixed here. Its remaining finding is the version-bump question addressed above. ## Public Consent is now recorded per agreement type, and the agreements endpoint lists every supported document with its current version and URL. Original PR: #954
1 parent 633ee21 commit 0bd5a28

24 files changed

Lines changed: 609 additions & 124 deletions

‎.stainless/stainless.yml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,13 @@ resources:
113113
business_customer_create_request: '#/components/schemas/BusinessCustomerCreateRequest'
114114
individual_customer_update_request: '#/components/schemas/IndividualCustomerUpdateRequest'
115115
business_customer_update_request: '#/components/schemas/BusinessCustomerUpdateRequest'
116+
agreement_type: '#/components/schemas/AgreementType'
117+
agreement_acceptance_method: '#/components/schemas/AgreementAcceptanceMethod'
116118
end_user_terms: '#/components/schemas/EndUserTerms'
117-
end_user_terms_consent: '#/components/schemas/EndUserTermsConsent'
118-
end_user_terms_consent_request: '#/components/schemas/EndUserTermsConsentRequest'
119+
agreement_document: '#/components/schemas/AgreementDocument'
120+
agreement_document_list_response: '#/components/schemas/AgreementDocumentListResponse'
121+
agreement_consent: '#/components/schemas/AgreementConsent'
122+
agreement_consent_request: '#/components/schemas/AgreementConsentRequest'
119123
# Internal-account management (list/update/export under this resource)
120124
internal_account_list_response: '#/components/schemas/InternalAccountListResponse'
121125
internal_account_update_request: '#/components/schemas/InternalAccountUpdateRequest'
@@ -142,6 +146,7 @@ resources:
142146
create_kyc_link:
143147
endpoint: post /customers/{customerId}/kyc-link
144148
body_param_name: KycLinkCreateRequest
149+
list_agreements: get /customers/agreements
145150
retrieve_end_user_terms: get /customers/end-user-terms
146151
# Subresources define resources that are nested within another for more powerful
147152
# logical groupings, e.g. `cards.payments`.

‎mintlify/changelog.mdx‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ Three card-statement corrections to `CardTransaction`:
4848
- `GET /transactions` accepts a `cardId` filter (`Card:` LSID or UUID) for listing one
4949
card of a multi-card cardholder, and rejects `status` combined with `type=CARD` with a
5050
400—`status` applies to payment transactions only.
51+
## Record consent for each agreement by type
52+
53+
`agreementConsents` supersedes `endUserTermsConsent`: a list with one entry per agreement,
54+
so a customer's acceptance of each document is recorded and auditable separately.
55+
56+
- Each entry carries a `type` alongside the existing `acceptedAt`, `ipAddress`,
57+
`termsVersion`, and `acceptanceMethod` fields. Send at most one entry per type.
58+
- `GET /customers/agreements` lists every supported agreement with its `type`, current
59+
`version`, and hosted `url`. Send an entry's `version` as `termsVersion` when recording
60+
that agreement's acceptance. `GET /customers/end-user-terms` is deprecated but unchanged
61+
— it still returns the single `{ version, url }` object, so existing callers keep working.
62+
- Customer responses return `agreementConsents`, holding the most recent acceptance per
63+
accepted type, and an empty list until the first acceptance.
64+
- On update, supplying consents records additional acceptances; acceptances already on
65+
file for other types are left untouched.
66+
67+
`endUserTermsConsent` is **deprecated, not removed** — it still works on both the request
68+
and response, and is equivalent to a single `LIGHTSPARK_END_USER_TERMS` entry. Acceptance
69+
you have already recorded is migrated for you and reported under that type; you don't need
70+
to re-collect it. Sending both fields in one request is rejected.
71+
72+
Accepting one agreement never implies acceptance of another, so the remaining six types
73+
must be collected from each customer.
74+
75+
See [Disclosures](/payouts-and-b2b/onboarding/disclosures).
5176

5277
## Card returns report as `SETTLED` with a `refundedAmount`
5378

‎mintlify/openapi.yaml‎

Lines changed: 157 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)