Skip to content

Add payment documents for payouts that need supporting evidence - #1070

Open
Awinfredwa wants to merge 11 commits into
mainfrom
codex/at-6686-payment-documents-spec
Open

Awinfredwa wants to merge 11 commits into
mainfrom
codex/at-6686-payment-documents-spec

Conversation

@Awinfredwa

@Awinfredwa Awinfredwa commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Some payouts need supporting documents before the partner will pay them out. A business payout in CNY to China is the first one. Each purpose of payment needs its own set of documents, such as an invoice, a contract, or a purchase order.

Today the API has no way to send those documents. This PR adds the contract for it.

How it works

A platform does three things:

  1. Finds out what is needed. The requirements are published in the docs, in a new page, Supporting Documents for China Business Payouts (mintlify/payouts-and-b2b/payment-flow/supporting-documents.mdx). They are the same for every China B2B payout, and the purpose of payment picks which row applies. The page lists the requirement IDs for each purpose and the document types each requirement accepts.
  2. Uploads the files. POST /payment-documents takes one file per request and returns a document ID. GET /payment-documents/{id} shows its status.
  3. Creates the quote with the IDs. POST /quotes takes up to 3 documentIds. Grid attaches every file to the payment before it returns the quote.

Quotes enforce the requirements. If documents are missing, POST /quotes returns 400 DOCUMENTS_REQUIRED, and details.missingRequirements lists the missing requirement IDs. That is the per-payout, machine-readable signal.

If any file fails to attach, Grid creates no quote. The platform retries the same request with the same Idempotency-Key.

Rules worth knowing:

  • Files are PDF, JPEG, or PNG, from 1 to 8,000,000 bytes.
  • A document can be used for 24 hours and on one quote only.
  • A document belongs to one customer. Only that customer's quotes can use it. Without a customerId, it belongs to the platform, for the platform's own quotes.
  • documentIds requires an Idempotency-Key.
  • A service payment picks the purpose that names the service: COMMISSION_ON_GOODS, COMMISSION_ON_SERVICES, ACCOUNTING_SERVICES, or EXHIBITION_SERVICES. SERVICE_CHARGES is not accepted on China B2B payouts.
  • Sending documentIds on a payout that does not use them is a 400. So is a China B2B payout whose purposeOfPayment is not one of the China business purposes in the docs.
  • The quote lists the IDs of its attached documents in a new documentIds field.
  • The quote also echoes the purposeOfPayment it was created with, in a new purposeOfPayment field.

New error codes

  • DOCUMENTS_REQUIRED (400): documents are missing. details.missingRequirements says which.
  • PAYMENT_DOCUMENT_NOT_FOUND (404)
  • PAYMENT_DOCUMENT_ALREADY_USED (409)
  • PAYMENT_DOCUMENT_EXPIRED (410)
  • PAYMENT_DOCUMENT_ATTACHMENT_FAILED (424): nothing was created, so retry.

Notes

  • Everything is additive, so info.version is unchanged.
  • The requirements live in the docs, not in an API field. They don't vary per payout, and integrators are expected to set expectations up front rather than drive their UX from a response. The external-account lookup is unchanged. /discoveries is unchanged too, since it lists banks. A machine-readable version can be added later if integrators ask for one.
  • The four service purposes replace a separate serviceCategory field. The payout partner has one purpose for all four services and takes the service name as free text. A second field on our side added a rule without matching anything the partner needs.
  • The China-specific purposes are no longer documented as rejected on other corridors. Grid sends the payout partner the closest purpose it accepts there, or the partner's "other" purpose when none fits. China B2B payouts still accept only the purposes listed in the docs.
  • This replaces the per-quote upload design in draft Add the payment-document contract for quotes that need supporting evidence #1045. It keeps that PR's document type enum.
  • Some response values are documented rather than fixed in the schema: the requirement IDs in missingRequirements, contentType, and sizeBytes. Generated clients validate enums and limits, so fixing them would make a new format, a larger file, or a new corridor a breaking change. Today's values are listed in each description.
  • The server side follows once this merges.

Checks

  • make build regenerates openapi.yaml and mintlify/openapi.yaml.
  • make lint-openapi passes.

🤖 Generated with Claude Code

China B2B CNY payouts need purpose-specific supporting documents. Platforms
upload each file with POST /payment-documents, then pass the ids in
documentIds on POST /quotes. Grid attaches every file before it returns the
quote; if any attachment fails, no quote is created and the request can be
retried with the same Idempotency-Key.

- GET /receiver/external-account/{accountId} returns paymentRequirements
- POST /payment-documents and GET /payment-documents/{paymentDocumentId}
- QuoteRequest: documentIds (1-3) and serviceCategory
- Quote: documents
- Error codes: DOCUMENTS_REQUIRED, PAYMENT_DOCUMENT_NOT_FOUND,
  PAYMENT_DOCUMENT_ALREADY_USED, PAYMENT_DOCUMENT_EXPIRED,
  PAYMENT_DOCUMENT_ATTACHMENT_FAILED

Part of AT-6686.

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

vercel Bot commented Sep 23, 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 25, 2026 4:41pm UTC
grid-flow-builder Ignored Ignored Preview Sep 25, 2026 4:41pm UTC
grid-wallet-demo Ignored Ignored Preview Sep 25, 2026 4:41pm UTC

Request Review

@mintlify

mintlify Bot commented Sep 23, 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 25, 2026, 4:42 PM

Later corridors may add requirement ids. A closed enum in a response breaks
strictly generated clients when a value is added, so the id is a documented
string instead.

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

Copy link
Copy Markdown
Contributor Author

@greptile-apps review

@greptile-apps

greptile-apps Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no outstanding or newly introduced actionable defect remains.

Summary

This PR adds the API contract for uploading supporting payment documents, discovering corridor-specific document requirements, and attaching uploaded documents when creating quotes.

  • Adds upload and retrieval endpoints for payment documents.
  • Exposes payment requirements through external-account lookup responses.
  • Extends quote requests and responses with document IDs, service categories, and attached-document metadata.
  • Adds document-specific errors and Stainless resource configuration.
  • Clarifies that advertised requirements never exceed the quote document limit while keeping evolving response values open for forward compatibility.
Diagram
sequenceDiagram
    participant Platform
    participant Grid
    participant Partner

    Platform->>Grid: "GET /receiver/external-account/{accountId}"
    Grid-->>Platform: paymentRequirements
    loop One request per required file
        Platform->>Grid: POST /payment-documents
        Grid-->>Platform: PaymentDocument ID
    end
    Platform->>Grid: POST /quotes with documentIds
    Grid->>Partner: Attach supporting documents
    alt Every attachment succeeds
        Grid-->>Platform: Quote with documents
    else Attachment fails
        Grid-->>Platform: 424 PAYMENT_DOCUMENT_ATTACHMENT_FAILED
    end
Loading

Reviews (2) · Last reviewed commit: "Leave payment document formats and sizes..."

Comment thread openapi/components/schemas/payment_documents/PaymentRequirements.yaml Outdated
maxDocuments and requiredDocuments could advertise more documents than
documentIds accepts on POST /quotes. Both now stop at 3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A maximum on a response field turns a later increase into a breaking change,
because generated clients validate it. documentIds keeps maxItems: 3 on the
request, which is safe to raise. maxDocuments and requiredDocuments now say
they never exceed that limit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
contentType, acceptedContentTypes, and sizeBytes were fixed to today's
formats and 8,000,000-byte cap. Generated clients validate those, so
accepting another format or a larger file later would break them. They are
now documented values, and the lookup stays the source of the current list.

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

@Awinfredwa Awinfredwa left a comment

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.

this is solid, once this flow is approved can be set ready

@Awinfredwa
Awinfredwa marked this pull request as ready for review September 24, 2026 00:48
A China B2B service payment needed two fields: SERVICE_CHARGES as the
purpose, plus serviceCategory to say which service. Thunes takes the
service as free text in additional_information_1. The second field
mirrored nothing on the provider side.

The four services are now purposes of their own: COMMISSION_ON_GOODS,
COMMISSION_ON_SERVICES, ACCOUNTING_SERVICES, and EXHIBITION_SERVICES.
Each needs the same documents SERVICE_CHARGES did. SERVICE_CHARGES stays
in the enum but is not accepted on China B2B payouts.

Removed: QuoteRequest.serviceCategory, the ChinaB2BServiceCategory schema,
and PurposeDocumentRequirements.serviceCategoryRequired.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
application/json:
schema:
$ref: ../../../components/schemas/receiver/ReceiverExternalAccountLookupResponse.yaml
examples:

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.

if this is static could it just be a table in docs? i can't imagine people would programatically collect documents but could be wrong
cc @pengying for thoughts as well

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.

yea this is purely hardcoded for Thune's requirement, they presented this as a table so do u mean we should just have something like this:

Image

@ls-bolt

ls-bolt Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

🦣 Congratulations @shreyav - your substantive review earned a Dromornis! (common)

This three-meter Australian bird had a massive skull but was probably herbivorous.

View your Frost-dex: https://zeus.dev.dev.sparkinfra.net/#/dinodex/shreyav?section=ice-age

The China-specific purposes were documented as rejected outside China.
Grid now sends the payout partner the closest purpose it accepts, or the
partner's "other" purpose when none fits. China B2B payouts still accept
only the purposes their paymentRequirements list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
POST /quotes treats a missing customerId as the platform itself, so a
platform paying its own supplier is the sender. Upload required a
customerId, so that platform could never supply documents.

customerId is now optional on POST /payment-documents. Omitted, the
document belongs to the platform and can be used only on the platform's
own quotes. A document the platform owns has no customerId in responses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
type: string
description: The external account ID that was looked up
example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
paymentRequirements:

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.

Will this be different on a per transaction basis? If not, it might be better in the discovery API + documentation than in the lookup API.

I would expect that integrators are setting up expectations to their customers about what's required to send to China vs dynamically driving their UX from these API responses

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.

Yes discovery makes more sense since they don't change per transaction, updating now, thanks!

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.

Good question. It doesn't vary per transaction. The requirements are the same for every China B2B payout, and only the purpose picks which row applies. The per-transaction check already happens at quote time: DOCUMENTS_REQUIRED lists the missing requirements. So this now drops paymentRequirements from the lookup and publishes the table in the docs (mintlify/payouts-and-b2b/payment-flow/supporting-documents.mdx). I didn't add it to /discoveries, since that endpoint lists banks. We can add a machine-readable version later if integrators ask for one.

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.

Went with docs only in the end: /discoveries lists banks per country/currency, but these requirements depend on rail, beneficiary type and purpose, which it can't express. Happy to add a machine-readable version there later if integrators ask.

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.

Ok, let's start with docs under the CNY external account. If integrators run into issues we can add an API.

@ls-bolt

ls-bolt Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

🦣 Congratulations @pengying - your substantive review earned a Woolly rhinoceros! (rare)

A broad front horn and thick coat suited it to grazing across cold Eurasian steppe.

View your Frost-dex: https://zeus.dev.dev.sparkinfra.net/#/dinodex/pengying?section=ice-age

rateDetails:
$ref: ../transactions/OutgoingRateDetails.yaml
description: Details about the rate and fees for the transaction.
documents:

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.

Do the documents need to turn into documentIds in the response? I don't think we need to change the data structure, and it's not a super RESTful pattern.

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.

fixing now, will send and return documentIds. Details stay available from GET /payment-documents/{id}.

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.

Agreed. Size isn't the concern (it's at most 3), but echoing the request shape is cleaner. The quote now returns documentIds, and QuoteDocument is gone. Details stay available from GET /payment-documents/{id}.

Comment thread openapi/openapi.yaml
$ref: paths/receiver/uma/receiver_uma_{receiverUmaAddress}.yaml
/receiver/external-account/{accountId}:
$ref: paths/receiver/external-account/receiver_external-account_{accountId}.yaml
/payment-documents:

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.

Does this need to be a different endpoint than documents?

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.

Yes i think this should. /documents takes kyb/kyc docs and passes them right to sumsub, no storage. payment documents in this case would need to be stored. also the types of documents are vastly different, /document is for identity verification and /payment-document is per transaction docs like receipts and invoices.

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 right. I was wondering if we wanted a single general document endpoint, but I guess there aren't that many document types?

Comment thread openapi/components/schemas/payment_documents/DocumentRequirement.yaml Outdated
The requirements are the same for every China business payout. Only the
purpose of payment picks which row applies. So the lookup no longer
returns paymentRequirements, and a new docs page lists the table instead.

POST /quotes still checks each payout and returns DOCUMENTS_REQUIRED with
details.missingRequirements when documents are missing.

- Drop paymentRequirements from GET /receiver/external-account/{accountId}
  and delete PaymentRequirements, PurposeDocumentRequirements and
  DocumentRequirement. Keep PaymentDocumentRequirementId for
  missingRequirements.
- Point the descriptions that named the lookup fields at the docs page.
- Add "Supporting Documents for China Business Payouts" under Payouts &
  B2B > Sending payments, and link it from the purpose of payment notes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The quote now echoes the request's shape: an array of payment document
IDs rather than objects with the ID and type. Drop the QuoteDocument
schema. Each document's details stay available from
GET /payment-documents/{paymentDocumentId}.

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

This branch was successfully deployed

1 active deployment
staging - mintlify — 5078535b Deployed Sep 25, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants