Skip to content

Add the payment-document contract for quotes that need supporting evidence - #1045

Closed
ls-bolt[bot] wants to merge 1 commit into
mainfrom
09-22-at-6686-payment-document-contract
Closed

ls-bolt[bot] wants to merge 1 commit into
mainfrom
09-22-at-6686-payment-document-contract

Conversation

@ls-bolt

@ls-bolt ls-bolt Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Some corridors will not clear a payment without evidence of what it is for. A business payout to China is the case driving this: the documents it needs vary by purposeOfPayment, and the payment cannot go out until they are all in. Today the API has no way to express that, so there is nothing a platform can build against.

This adds the contract. A quote that needs documents comes back with documentStatus: PENDING and a documentRequirements array, and a new endpoint uploads one file per requirement:

  1. POST /quotes returns the quote with its requirements and no paymentInstructions.
  2. POST /quotes/{quoteId}/documents uploads one file, naming the requirement it satisfies and what the file is.
  3. The response is the updated quote, so the caller watches satisfied flip per requirement.
  4. The upload that satisfies the last requirement flips documentStatus to COMPLETE, after which the quote can be executed.

A quote that needs no documents omits both fields, so nothing changes for existing integrations.

This is the contract only. The handler, persistence, and provider calls are webdev work — see the dependency note below.

Design notes

One file per requirement. acceptedDocumentTypes lists alternatives for a single file, not a list of files to send. A SUPPORTING_PROOF requirement accepting PURCHASE_ORDER or DELIVERY_SLIP still takes one upload; documentType says which one you sent.

Requirement ID and document type are separate enums. The ID names the slot to fill and is a stable policy key; the type names what the file is. They coincide often enough to look redundant, but diverge exactly where a requirement accepts alternatives — COMMERCIAL_AGREEMENT is satisfied by a purchase order, invoice, or contract. Both are closed enums so a generated client cannot send an arbitrary key.

serviceCategory is optional in the schema. It is required at runtime only for a China B2B CNY payment whose purposeOfPayment is SERVICE_CHARGES. OpenAPI cannot express a route-specific conditional cleanly, so the field stays optional and the description carries the condition, with the failure documented as 400 INVALID_INPUT on POST /quotes.

Uploads are final. The first file accepted for a requirement satisfies it; there is no replace or delete. Correcting a wrong file means creating a new quote. This keeps the contract honest about what the provider has already been sent.

Document types are logical, not provider enums. PaymentDocumentType describes the evidence, not a Thunes attachment type. The mapping is unresolved for customs declarations, logistics bills, and bills of lading, and belongs to the webdev implementation.

Changes: 17 files

New reusable schemas under openapi/components/schemas/quotes/:

  • QuoteDocumentStatus — PENDING / COMPLETE
  • QuoteDocumentRequirement — requirementId, non-empty acceptedDocumentTypes, satisfied
  • QuoteDocumentRequirementId — the 12 policy keys
  • PaymentDocumentType — the 10 logical evidence types
  • ChinaB2BServiceCategory — the 4 service categories
  • QuoteDocumentUploadRequest / QuoteDocumentUploadRequestBody — the multipart body

Modified:

  • openapi/paths/quotes/quotes_{quoteId}_documents.yaml (new) — uploadQuoteDocument, tagged Cross-Currency Transfers, BasicAuth, responses 200/400/401/404/409/410/500
  • openapi/openapi.yaml — register the route
  • Quote.yaml — optional documentStatus and documentRequirements; paymentInstructions notes it is absent while PENDING
  • QuoteRequest.yaml — optional serviceCategory; immediatelyExecute documents the unsupported combination
  • quotes.yaml — the 400 names both new causes, the 201 describes the PENDING shape, plus a China B2B request example
  • quotes_{quoteId}_execute.yaml — the existing 409 now names DOCUMENTS_REQUIRED
  • Error409.yaml — DOCUMENTS_REQUIRED, DOCUMENT_REQUIREMENT_ALREADY_SATISFIED, DOCUMENT_UPLOAD_IN_PROGRESS, QUOTE_NOT_ACCEPTING_DOCUMENTS
  • Error410.yaml — QUOTE_EXPIRED
  • .stainless/stainless.yml — map the new endpoint to a quotes.upload_document SDK method, and register QuoteDocumentRequirement as a named model
  • openapi.yaml, mintlify/openapi.yaml — regenerated

Additive throughout, so info.version is unchanged.

Test plan

Node 22. Commands and results:

  • make build — bundles regenerate; the two bundles are byte-identical.
  • make lint-openapi — exit 0. Redocly: "Your API description is valid", 2 warnings. Diffing every Redocly and Spectral finding against origin/main gives exactly one new line: an information-level "missing example" on QuoteDocumentUploadRequest.properties.file. The existing BaseDocumentRequest.properties.file carries the identical finding — a binary field has no sensible example, so this matches the house convention.
  • Bundled root and Mintlify specs both contain /quotes/{quoteId}/documents and all seven new schemas, each as a stable top-level component name (no inline anonymous objects, no _1 suffixes) — so the webdev Python client gets predictable names.
  • Every example in the spec validated against its own schema with ajv 2020: 128 examples, all valid, confirming the pre-existing quote examples still pass.
  • The full sequence above walked through as data and validated at each step: create → PENDING with 3 requirements and no instructions → three uploads → COMPLETE with instructions available. Also checked that a quote omitting both fields is valid, and that the schema rejects an unknown requirementId, an unknown document type, and an empty acceptedDocumentTypes.
  • git diff --check passes. The only deleted lines in the generated bundle are the three descriptions this PR intentionally rewrites; everything else is pure insertion.

Adversarial review flagged that the multipart file part had no encoding.contentType, so it would default to application/octet-stream — which this endpoint rejects. Fixed: the encoding now declares application/pdf, image/jpeg, image/png.

Dependencies and follow-ups

Depends on the China B2B purposes of payment (#1041), already merged.

The runtime behind this contract is webdev #35818 (purpose values and CNY guard), #35903 (requirement discovery), and #35822 (single attachment upload). Those are unchanged by this PR. The enum values here were cross-checked against #35903's policy module so the contract matches what the runtime will actually return.

On .stainless/stainless.yml

I initially left this out as out-of-scope, reasoning that the file is maintained out-of-band here — POST /cards/{id}/tokenize and the sandbox card-dispute simulators are absent from it too. CI disagreed, and it was right: the Stainless preview reported Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: post /quotes/{quoteId}/documents on all 8 SDK targets, each flagged as "a regression from the base state". The precedent I leaned on didn't actually cover this case — #985's endpoints produced no skip diagnostic.

So the endpoint is now mapped as quotes.upload_document. The name follows the documents resource, which is the closest analogue (also multipart, also an upload) and uses the same endpoint + body_param_name pair. That key becomes the public method name in all eight SDKs, so rename it here if you'd prefer something else — it's cheaper to change now than after an SDK release.

Fixing that surfaced a second, different diagnostic: Model/Recommended for QuoteDocumentRequirement. It is registered as quote_document_requirement, so the requirement object gets a named type in each SDK instead of an anonymous inline one — which is what the "stable names for the webdev Python client" requirement needs. The new enums are deliberately not registered: this repo registers almost no enums as models (TransactionStatus is the lone exception), and Stainless did not ask for them.

Public

Quotes that require supporting documents now report what is needed and accept uploads at POST /quotes/{quoteId}/documents.

@mintlify

mintlify Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated
Grid 🟢 Ready View Preview Sep 22, 2026, 5:59 PM

@ls-bolt ls-bolt Bot added the bolt label Sep 22, 2026
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

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

3 Skipped Deployments
Project Deployment Actions Updated
grid-cards-demo Ignored Ignored Preview Sep 22, 2026 5:58pm UTC
grid-flow-builder Ignored Ignored Preview Sep 22, 2026 5:58pm UTC
grid-wallet-demo Ignored Ignored Preview Sep 22, 2026 5:58pm UTC

Request Review

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Sep 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(api): add service-category parameter to quotes create methods

go

feat(api): add document requirements/status and service category to quotes

kotlin

feat(api): add documentRequirements/documentStatus to Quote, serviceCategory to QuoteRequest

openapi

feat(api): add quote document upload endpoint and fields, serviceCategory parameter

php

feat(api): add serviceCategory param, document requirement fields/types to quotes

python

feat(api): add service_category param, document fields to quotes

ruby

feat(api): add document_requirements/document_status to Quote, service_category to create

typescript

feat(api): add documentRequirements, documentStatus fields, serviceCategory param to quotes

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 "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

✅ grid-openapi studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗

✅ grid-kotlin studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ✅) → test ⏭️ (prev: test ❗)

✅ grid-ruby studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

✅ grid-go studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

go get github.com/stainless-sdks/grid-go@e6981bec22f7df730dfdfe620b6af9541ceca5cf
✅ grid-python studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️

✅ grid-php studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → lint ⏭️ → test ⏭️

✅ grid-cli studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗ → build ⏭️ → lint ⏭️ → test ⏭️


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-09-22 18:02:05 UTC

@ls-bolt

ls-bolt Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

⚡ Revision log — updated in place, latest first.

Revision 2

  • Registered QuoteDocumentRequirement as the quote_document_requirement model in .stainless/stainless.yml. The previous push cleared the Endpoint/NotConfigured skip (8 targets → 0), which then exposed a different note: Model/Recommended for that schema. Without it the requirement object generates as an anonymous inline type per SDK rather than a named one — directly relevant to the stable-names-for-the-Python-client requirement.
  • The new enums are intentionally left unregistered: this repo registers almost no enums as models (TransactionStatus is the only one), and Stainless raised no diagnostic for them.
  • Spec bundles are byte-identical again — this push touches only the SDK config. make build + make lint-openapi green, same single informational finding.
Earlier revisions (1)

Revision 1

  • Mapped the new endpoint in .stainless/stainless.yml as quotes.upload_document. I had left this out of the first revision as out-of-scope, but the Stainless preview flagged Endpoint/NotConfigured on all 8 SDK targets as a regression from base, so the generated clients really would have shipped without the upload method. Named after the documents resource, the closest analogue — same endpoint + body_param_name shape. That key becomes the public method name in every SDK, so say the word if you want it spelled differently.
  • No spec change in this push: the bundles are byte-identical to the previous revision, and make build + make lint-openapi stay green with the same single informational finding.

…dence

Some corridors will not clear a payment without evidence of what it is for. A
business payout to China is the case driving this: the documents it needs vary
by purposeOfPayment, and the payment cannot go out until they are all in.

A quote that needs documents now comes back with documentStatus: PENDING and a
documentRequirements array, and POST /quotes/{quoteId}/documents uploads one
file per requirement. The response is the updated quote, so the caller watches
satisfied flip per requirement and documentStatus reach COMPLETE on the last
one. A quote that needs no documents omits both fields.

QuoteRequest gains an optional serviceCategory, which a China B2B CNY
SERVICE_CHARGES payment requires at runtime. The rule is route-specific and
OpenAPI cannot express it cleanly, so the field stays optional in the schema
and the description carries the condition.

Contract only. The handler, persistence, and provider calls are webdev work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
staging - mintlify — 4dd5518f Deployed Sep 22, 2026 by mintlify[bot]
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.

2 participants