Add payment documents for payouts that need supporting evidence - #1070
Awinfredwa wants to merge 11 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
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>
|
@greptile-apps review |
|
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
left a comment
There was a problem hiding this comment.
this is solid, once this flow is approved can be set ready
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: |
There was a problem hiding this comment.
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
|
🦣 Congratulations @shreyav - your substantive review earned a Dromornis! (common)
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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yes discovery makes more sense since they don't change per transaction, updating now, thanks!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ok, let's start with docs under the CNY external account. If integrators run into issues we can add an API.
|
🦣 Congratulations @pengying - your substantive review earned a Woolly rhinoceros! (rare)
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
fixing now, will send and return documentIds. Details stay available from GET /payment-documents/{id}.
There was a problem hiding this comment.
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}.
| $ref: paths/receiver/uma/receiver_uma_{receiverUmaAddress}.yaml | ||
| /receiver/external-account/{accountId}: | ||
| $ref: paths/receiver/external-account/receiver_external-account_{accountId}.yaml | ||
| /payment-documents: |
There was a problem hiding this comment.
Does this need to be a different endpoint than documents?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
All right. I was wondering if we wanted a single general document endpoint, but I guess there aren't that many document types?
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>

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:
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.POST /payment-documentstakes one file per request and returns a document ID.GET /payment-documents/{id}shows its status.POST /quotestakes up to 3documentIds. Grid attaches every file to the payment before it returns the quote.Quotes enforce the requirements. If documents are missing,
POST /quotesreturns400 DOCUMENTS_REQUIRED, anddetails.missingRequirementslists 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:
customerId, it belongs to the platform, for the platform's own quotes.documentIdsrequires anIdempotency-Key.COMMISSION_ON_GOODS,COMMISSION_ON_SERVICES,ACCOUNTING_SERVICES, orEXHIBITION_SERVICES.SERVICE_CHARGESis not accepted on China B2B payouts.documentIdson a payout that does not use them is a400. So is a China B2B payout whosepurposeOfPaymentis not one of the China business purposes in the docs.documentIdsfield.purposeOfPaymentit was created with, in a newpurposeOfPaymentfield.New error codes
DOCUMENTS_REQUIRED(400): documents are missing.details.missingRequirementssays 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
info.versionis unchanged./discoveriesis unchanged too, since it lists banks. A machine-readable version can be added later if integrators ask for one.serviceCategoryfield. 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.missingRequirements,contentType, andsizeBytes. 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.Checks
make buildregeneratesopenapi.yamlandmintlify/openapi.yaml.make lint-openapipasses.🤖 Generated with Claude Code