feat(api): add account-to-account transfer endpoint#19
Merged
marioalvial merged 2 commits intomainfrom May 6, 2026
Merged
Conversation
Adds POST /api/operations/transfer for moving funds between two accounts of the same customer. The referenced quote determines whether it is a same-asset book transfer (1:1 spot) or a cross-asset FX transfer. - New OpenAPI path with 201/400/401/404/409/422 responses - New schemas: CreateTransferRequest, OperationTransferIntent - OperationIntent oneOf + discriminator extended with TRANSFER variant - New shared example: TargetAccountNotFound (canonical RESOURCE_NOT_FOUND shape, target-account UUID) - Distinct error codes SOURCE_ASSET_NOT_ENABLED and TARGET_ASSET_NOT_ENABLED (capability check on accounts, separate from withdrawal's INVALID_SOURCE_ASSET which is a quote-vs-instruction mismatch) - Journey guide journeys/transfer.mdx (3 steps: quote, transfer, track) - API Reference stub api-reference/fx-payment/operations/create-transfer.mdx - Navigation entries in docs.json
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Same-asset transfers no longer require a pre-created quote. The request
takes amount {value, asset} directly; the system generates a 1:1 quote
internally and returns it on the response so the OperationResponse shape
stays unchanged. quoteId remains accepted as an optional field, reserved
for future use.
- CreateTransferRequest: amount is required, quoteId is optional
- Path description and journey updated to remove the quote prerequisite
- Journey collapses from 3 steps to 2 (transfer, track)
- 201 response example shows the system-generated 1:1 quote
- 400 missingQuoteId replaced with missingAmount
- 404 quoteNotFound dropped (no customer-supplied quote to look up)
- 422 quoteExpired/quoteAlreadyConsumed dropped (no quote consumption)
- Asset-not-enabled details key normalised from quoteSourceAsset/
quoteTargetAsset to asset (single asset on the wire)
- OperationResponse.quote stays required; description notes the
transfer-side generation
felipespadua
approved these changes
May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
POST /api/operations/transfer— a new operation type for moving funds between two accounts of the same customer. The referenced quote determines whether it is a same-asset book transfer (1:1 spot) or a cross-asset FX transfer (e.g. BRL on the source account → USD on the target). No external rail is involved.The shape mirrors the existing operation siblings (withdrawal/swap/deposit):
accountIdon the operation root represents the source account, the target is carried as a snapshot onintent.targetAccount— symmetric to how withdrawal carriesintent.beneficiaryand deposit carriesintent.fundingInstruction.Cross-customer transfers are intentionally out of scope for this slice — same pattern as withdrawal shipping
REFERENCE-only first.Key Changes
apis/fx-payment/openapi.yml— new pathPOST /api/operations/transferwith full 201/400/401/404/409/422 coverage; new schemasCreateTransferRequest(sourceAccountId,targetAccountId,quoteId) andOperationTransferIntent;OperationIntentoneOf+discriminator.mappingextended withTRANSFER;OperationResponse.descriptionupdated to list transfers as the 4th sibling.TargetAccountNotFound— added tocomponents.examplesmirroring the canonicalRESOURCE_NOT_FOUNDshape with the target-account UUID. Replaces an inline example that used a non-existentACCOUNT_NOT_FOUNDcode (caught during simplify review).SOURCE_ASSET_NOT_ENABLED/TARGET_ASSET_NOT_ENABLED— chosen over reusing withdrawal'sINVALID_SOURCE_ASSET, which has different semantics (quote-vs-instruction mismatch) and a differentdetailspayload. The new codes describe the account-capability check more precisely and mirror deposit'sRAIL_NOT_AVAILABLEstyle.journeys/transfer.mdx— new guide following the journey template (Overview, Prerequisites, Steps, What happens next), with same-asset and cross-asset examples and a Warning on the cross-customer constraint.api-reference/fx-payment/operations/create-transfer.mdx— one-line OpenAPI stub registering the auto-generated reference page.docs.json—journeys/transferadded to the Journeys group;api-reference/fx-payment/operations/create-transferadded to the API Reference → Operations group.Type of change
How Has This Been Tested?
Validated locally with
mint validate(passes) andmint broken-links(passes). Three independent review agents (reuse / quality / efficiency) ran against the diff before commit; all consensus findings were applied — including the wrong-error-code bug and the prose-trim wins. The remaining suggestions (extractingMissingQuoteIdto a shared component, extracting atrack-operationsnippet) touch all four operation endpoints and are out of scope for this PR.Checklist:
Backend / infra follow-up
The live
Intentsealed class infx-payment/core/.../domain/Intent.ktcurrently has onlyWithdrawal | Deposit | Swap. Implementing this contract in code requires:data class Transfer(val targetAccount: AccountReference) : Intent()variant on the sealed class, plus aTransferPlanner(sibling ofWithdrawalPlanner/SwapPlanner/DepositPlanner).SAME_SOURCE_AND_TARGET,TARGET_ACCOUNT_NOT_ACTIVE,SOURCE_ASSET_NOT_ENABLED,TARGET_ASSET_NOT_ENABLED.OPERATION_REQUESTEDwebhook event fires on creation as for the other three operations; lifecycle transitions remain unpublished.aspirational endpoints in docs ahead of implementationconvention applies — this PR can merge before the code lands.