From 9dbd706a9e6d9de27405663059eafe05a9b96d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Alvial?= Date: Wed, 6 May 2026 13:47:36 -0300 Subject: [PATCH 1/2] feat(api): add account-to-account transfer endpoint 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 --- .../fx-payment/operations/create-transfer.mdx | 5 + apis/fx-payment/openapi.yml | 286 +++++++++++++++++- docs.json | 4 +- journeys/transfer.mdx | 75 +++++ 4 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 api-reference/fx-payment/operations/create-transfer.mdx create mode 100644 journeys/transfer.mdx diff --git a/api-reference/fx-payment/operations/create-transfer.mdx b/api-reference/fx-payment/operations/create-transfer.mdx new file mode 100644 index 0000000..f726a56 --- /dev/null +++ b/api-reference/fx-payment/operations/create-transfer.mdx @@ -0,0 +1,5 @@ +--- +title: "Create a transfer" +description: "Moves funds between two accounts of the same customer using a locked quote." +openapi: "apis/fx-payment/openapi.yml POST /api/operations/transfer" +--- diff --git a/apis/fx-payment/openapi.yml b/apis/fx-payment/openapi.yml index 09f1f39..d538d39 100644 --- a/apis/fx-payment/openapi.yml +++ b/apis/fx-payment/openapi.yml @@ -973,6 +973,36 @@ components: example: "PIX_KEY" required: - rail + CreateTransferRequest: + type: object + description: > + References a quote previously obtained from `POST /api/quotes`. Both accounts must + belong to the authenticated customer. + properties: + sourceAccountId: + type: string + format: uuid + description: Source account that will be debited. Must match the account on the referenced quote. + example: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + targetAccountId: + type: string + format: uuid + description: > + Target account that will be credited. Must belong to the same customer as the + source account, must be in `ACTIVE` status, and must have the quote's target + asset enabled. Cannot equal `sourceAccountId`. + example: "b2c3d4e5-6f70-8901-bcde-f12345678901" + quoteId: + type: string + format: uuid + description: > + ID of a quote returned by `POST /api/quotes`. Must belong to the same customer, + must not have expired, and must not have been consumed by another operation. + example: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + required: + - sourceAccountId + - targetAccountId + - quoteId WithdrawalBeneficiary: description: | Beneficiary for the withdrawal. Discriminated by `type`. @@ -1088,8 +1118,9 @@ components: type: object description: > An operation. Withdrawals (`POST /api/operations/withdrawal`), swaps - (`POST /api/operations/swap`), and deposits (`POST /api/operations/deposit`) - share this shape, discriminated by `intent.type`. + (`POST /api/operations/swap`), deposits (`POST /api/operations/deposit`), + and transfers (`POST /api/operations/transfer`) share this shape, discriminated + by `intent.type`. properties: id: type: string @@ -1222,12 +1253,14 @@ components: - $ref: "#/components/schemas/OperationWithdrawalIntent" - $ref: "#/components/schemas/OperationSwapIntent" - $ref: "#/components/schemas/OperationDepositIntent" + - $ref: "#/components/schemas/OperationTransferIntent" discriminator: propertyName: type mapping: WITHDRAWAL: "#/components/schemas/OperationWithdrawalIntent" SWAP: "#/components/schemas/OperationSwapIntent" DEPOSIT: "#/components/schemas/OperationDepositIntent" + TRANSFER: "#/components/schemas/OperationTransferIntent" OperationWithdrawalIntent: title: Withdrawal type: object @@ -1270,6 +1303,22 @@ components: required: - type - fundingInstruction + OperationTransferIntent: + title: Transfer + type: object + description: > + Intent for a transfer. Carries a snapshot of the target account at the time the + operation was created. + properties: + type: + type: string + enum: + - TRANSFER + targetAccount: + $ref: "#/components/schemas/AccountReference" + required: + - type + - targetAccount OperationFundingInstruction: description: Funding instruction the customer uses to send money in. Discriminated by `rail`. oneOf: @@ -2223,6 +2272,15 @@ components: resource: "Account" parameters: id: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + TargetAccountNotFound: + summary: Target account does not exist or is not owned by the authenticated customer + value: + code: "RESOURCE_NOT_FOUND" + message: "Account with given parameters [id:b2c3d4e5-6f70-8901-bcde-f12345678901] not found" + details: + resource: "Account" + parameters: + id: "b2c3d4e5-6f70-8901-bcde-f12345678901" InstructionNotFound: summary: Payment instruction does not exist on the beneficiary value: @@ -2937,6 +2995,230 @@ paths: quoteAlreadyConsumed: $ref: "#/components/examples/QuoteAlreadyConsumed" + # ── Operations: Transfers ───────────────────────────────────────── + /api/operations/transfer: + post: + operationId: createTransfer + tags: + - Operations + summary: Create a transfer + description: > + Creates a transfer that debits one customer account and credits another. Both accounts + must belong to the authenticated customer. The referenced quote determines `sourceAmount` + and `targetAmount` (including their assets) — same-asset transfers use a 1:1 spot quote, + and cross-asset transfers use a quote whose source and target assets differ. Returns + `201` immediately with the operation in `REQUESTED` status; settlement happens + asynchronously. + + Cross-customer transfers are not supported in the current slice — the target account + must be owned by the same customer as the source account. + parameters: + - $ref: "#/components/parameters/IdempotencyKey" + - $ref: "#/components/parameters/TraceVersion" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateTransferRequest" + examples: + sameAssetBrl: + summary: BRL → BRL transfer between two accounts of the same customer + value: + sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" + quoteId: "d4e5f6a7-b8c9-0123-defa-2345678901bc" + brlToUsd: + summary: BRL on source → USD on target (FX transfer) + value: + sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" + quoteId: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + responses: + "201": + description: Transfer accepted. The operation is in `REQUESTED` status. + headers: + X-Request-Id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/OperationResponse" + examples: + brlToUsdTransfer: + summary: BRL → USD transfer in REQUESTED state + value: + id: "4c6d1f10-5b4d-7e6d-cf5b-ae4d7e6dcf5b" + customerId: "11111111-1111-1111-1111-111111111111" + account: + id: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + owner: "Acme Importação Ltda" + sourceAmount: + value: "5000.00" + asset: "BRL" + decimals: 2 + targetAmount: + value: "1000.00" + asset: "USD" + decimals: 2 + fees: [] + transactions: [] + tags: + - key: "psp" + value: "Amazon" + - key: "compliance-tier" + value: "high" + currentState: + status: "REQUESTED" + createdAt: "2026-05-06T16:24:11Z" + states: + - status: "REQUESTED" + createdAt: "2026-05-06T16:24:11Z" + intent: + type: "TRANSFER" + targetAccount: + id: "b2c3d4e5-6f70-8901-bcde-f12345678901" + owner: "Acme Importação Ltda" + quote: + id: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + effectiveRate: + value: "0.20" + base: "BRL" + quote: "USD" + expiresAt: "2026-05-06T16:24:41Z" + consumedAt: null + createdAt: "2026-05-06T16:24:11Z" + updatedAt: "2026-05-06T16:24:11Z" + "400": + description: "Validation error: missing or malformed fields." + headers: + X-Request-Id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + missingSourceAccountId: + summary: sourceAccountId is missing + value: + code: "INVALID_DATA" + message: "Object contains invalid data" + details: + errors: + - code: "REQUIRED" + message: "Parameter 'body:sourceAccountId' not found in request" + field: "body:sourceAccountId" + params: {} + missingTargetAccountId: + summary: targetAccountId is missing + value: + code: "INVALID_DATA" + message: "Object contains invalid data" + details: + errors: + - code: "REQUIRED" + message: "Parameter 'body:targetAccountId' not found in request" + field: "body:targetAccountId" + params: {} + invalidSourceAccountUuid: + summary: sourceAccountId is not a UUID + value: + code: "INVALID_DATA" + message: "Object contains invalid data" + details: + errors: + - code: "INVALID_UUID" + message: "'not-a-uuid' is not a valid UUID" + field: "body:sourceAccountId" + params: {} + missingQuoteId: + summary: quoteId is missing + value: + code: "INVALID_DATA" + message: "Object contains invalid data" + details: + errors: + - code: "REQUIRED" + message: "Parameter 'body:quoteId' not found in request" + field: "body:quoteId" + params: {} + "401": + $ref: "#/components/responses/UnauthorizedError" + "404": + description: Source account, target account, or quote not found for this customer. + headers: + X-Request-Id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + sourceAccountNotFound: + $ref: "#/components/examples/AccountNotFound" + targetAccountNotFound: + $ref: "#/components/examples/TargetAccountNotFound" + quoteNotFound: + $ref: "#/components/examples/QuoteNotFound" + "409": + description: Idempotency key was reused with a different request body. + headers: + X-Request-Id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + idempotencyConflict: + $ref: "#/components/examples/IdempotencyConflict" + "422": + description: Business rule violation. + headers: + X-Request-Id: + $ref: "#/components/headers/RequestId" + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + sameSourceAndTarget: + summary: Source and target accounts are the same + value: + code: "SAME_SOURCE_AND_TARGET" + message: "Source and target accounts must differ" + details: + accountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + targetAccountNotActive: + summary: Target account is not in ACTIVE status + value: + code: "TARGET_ACCOUNT_NOT_ACTIVE" + message: "Target account is not active" + details: + targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" + status: "OPENING" + sourceAssetNotEnabled: + summary: Quote's source asset is not enabled on the source account + value: + code: "SOURCE_ASSET_NOT_ENABLED" + message: "Source asset is not enabled on the source account" + details: + quoteSourceAsset: "USD" + sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" + targetAssetNotEnabled: + summary: Quote's target asset is not enabled on the target account + value: + code: "TARGET_ASSET_NOT_ENABLED" + message: "Target asset is not enabled on the target account" + details: + quoteTargetAsset: "USD" + targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" + quoteExpired: + $ref: "#/components/examples/QuoteExpired" + quoteAlreadyConsumed: + $ref: "#/components/examples/QuoteAlreadyConsumed" + # ── Quotes ──────────────────────────────────────────────────────── /api/quotes: post: diff --git a/docs.json b/docs.json index a02191f..3298212 100644 --- a/docs.json +++ b/docs.json @@ -77,7 +77,8 @@ "journeys/open-multi-currency-account", "journeys/deposit", "journeys/withdrawal", - "journeys/swap" + "journeys/swap", + "journeys/transfer" ] } ] @@ -135,6 +136,7 @@ "api-reference/fx-payment/operations/create-withdrawal", "api-reference/fx-payment/operations/create-swap", "api-reference/fx-payment/operations/create-deposit", + "api-reference/fx-payment/operations/create-transfer", "api-reference/fx-payment/operations/get-operation", "api-reference/fx-payment/operations/list-operations" ] diff --git a/journeys/transfer.mdx b/journeys/transfer.mdx new file mode 100644 index 0000000..04cc64c --- /dev/null +++ b/journeys/transfer.mdx @@ -0,0 +1,75 @@ +--- +title: "Make a transfer" +description: "Step-by-step guide to moving funds between two accounts of the same customer." +--- + +## Overview + +Transfers move funds between two accounts owned by the same customer, optionally converting assets in transit. Every transfer references a quote that locks the FX rate (or a 1:1 spot for same-asset). + +## Prerequisites + +- Two [active accounts](/journeys/open-multi-currency-account) owned by the same customer. The source account must hold the quote's source asset; the target account must have the quote's target asset enabled. +- Valid [authentication credentials](/guides/authentication). + +## Steps + + + + Quotes lock the FX rate (or a 1:1 spot for same-asset) for a short window and are bound to the source account. Provide either `sourceAmount` or `targetAmount`, never both. For a same-asset transfer set `sourceAsset` and `targetAsset` to the same value. + + ```bash + curl --request POST \ + --url {{sandboxUrl}}/api/quotes \ + --header 'Authorization: Bearer ' \ + --header 'X-Idempotency-Key: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "accountId": "", + "sourceAsset": "BRL", + "targetAsset": "USD", + "sourceAmount": "5000.00" + }' + ``` + + The response returns the quote `id`, the locked `effectiveRate`, the computed `targetAmount`, and `expiresAt`. The quote can be consumed by exactly one operation before it expires. + + + + Reference the source account, the target account, and the quote. Source and target must differ; the target account must be in `ACTIVE` status with the quote's target asset enabled. + + ```bash + curl --request POST \ + --url {{sandboxUrl}}/api/operations/transfer \ + --header 'Authorization: Bearer ' \ + --header 'X-Idempotency-Key: ' \ + --header 'Content-Type: application/json' \ + --data '{ + "sourceAccountId": "", + "targetAccountId": "", + "quoteId": "" + }' + ``` + + Returns `201` immediately with the operation in `REQUESTED` status. Settlement happens asynchronously. + + + + Poll `GET /api/operations/{operationId}` to follow the operation through `PROCESSING` to `COMPLETED` (or `FAILED`). Lifecycle transitions are not published as webhook events; the `OPERATION_REQUESTED` webhook only fires on creation. + + ```bash + curl --request GET \ + --url {{sandboxUrl}}/api/operations/ \ + --header 'Authorization: Bearer ' + ``` + + + + + Cross-customer transfers are not supported — both accounts must belong to the same customer. If the target account belongs to another customer, the API returns `404 RESOURCE_NOT_FOUND`. + + +## What happens next + +- [Execute a swap](/journeys/swap) — convert between assets within a single account. +- [Make a withdrawal](/journeys/withdrawal) — send funds out to an external beneficiary. From e2572e6129c5ca0e198e4dd43bb3b1c71b6448db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Alvial?= Date: Wed, 6 May 2026 15:10:22 -0300 Subject: [PATCH 2/2] refactor(api): drive transfer request with amount instead of quoteId 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 --- apis/fx-payment/openapi.yml | 106 +++++++++++++++++++----------------- journeys/transfer.mdx | 31 +++-------- 2 files changed, 62 insertions(+), 75 deletions(-) diff --git a/apis/fx-payment/openapi.yml b/apis/fx-payment/openapi.yml index d538d39..93b6a00 100644 --- a/apis/fx-payment/openapi.yml +++ b/apis/fx-payment/openapi.yml @@ -976,33 +976,46 @@ components: CreateTransferRequest: type: object description: > - References a quote previously obtained from `POST /api/quotes`. Both accounts must - belong to the authenticated customer. + Same-asset transfer between two accounts of the same customer. The asset on `amount` + must be enabled on both accounts. No FX is performed — `sourceAmount` and `targetAmount` + on the response are equal to `amount`. The system generates a 1:1 quote internally + and returns it on `quote`; callers normally do not provide `quoteId`. properties: sourceAccountId: type: string format: uuid - description: Source account that will be debited. Must match the account on the referenced quote. + description: Source account that will be debited. example: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" targetAccountId: type: string format: uuid description: > Target account that will be credited. Must belong to the same customer as the - source account, must be in `ACTIVE` status, and must have the quote's target - asset enabled. Cannot equal `sourceAccountId`. + source account, must be in `ACTIVE` status, and must have `amount.asset` enabled. + Cannot equal `sourceAccountId`. example: "b2c3d4e5-6f70-8901-bcde-f12345678901" + amount: + type: object + description: Amount to move. The same value is debited from the source and credited to the target. + properties: + value: + $ref: "#/components/schemas/AmountValue" + asset: + $ref: "#/components/schemas/Asset" + required: + - value + - asset quoteId: type: string format: uuid description: > - ID of a quote returned by `POST /api/quotes`. Must belong to the same customer, - must not have expired, and must not have been consumed by another operation. - example: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + Optional. ID of a quote returned by `POST /api/quotes`. Reserved for future use; + same-asset transfers do not require one and generate a 1:1 quote internally. + example: "d4e5f6a7-b8c9-0123-defa-2345678901bc" required: - sourceAccountId - targetAccountId - - quoteId + - amount WithdrawalBeneficiary: description: | Beneficiary for the withdrawal. Discriminated by `type`. @@ -1120,7 +1133,8 @@ components: An operation. Withdrawals (`POST /api/operations/withdrawal`), swaps (`POST /api/operations/swap`), deposits (`POST /api/operations/deposit`), and transfers (`POST /api/operations/transfer`) share this shape, discriminated - by `intent.type`. + by `intent.type`. The `quote` field is always present; for transfers without an + explicit `quoteId` in the request, the system generates a 1:1 same-asset quote. properties: id: type: string @@ -3003,12 +3017,12 @@ paths: - Operations summary: Create a transfer description: > - Creates a transfer that debits one customer account and credits another. Both accounts - must belong to the authenticated customer. The referenced quote determines `sourceAmount` - and `targetAmount` (including their assets) — same-asset transfers use a 1:1 spot quote, - and cross-asset transfers use a quote whose source and target assets differ. Returns - `201` immediately with the operation in `REQUESTED` status; settlement happens - asynchronously. + Creates a same-asset transfer that debits one customer account and credits another. + Both accounts must belong to the authenticated customer and must have `amount.asset` + enabled. No FX is performed — the same amount is moved on both legs. The system + generates a 1:1 quote internally and returns it on `quote`; passing `quoteId` is + optional and reserved for future use. Returns `201` immediately with the operation + in `REQUESTED` status; settlement happens asynchronously. Cross-customer transfers are not supported in the current slice — the target account must be owned by the same customer as the source account. @@ -3022,18 +3036,14 @@ paths: schema: $ref: "#/components/schemas/CreateTransferRequest" examples: - sameAssetBrl: - summary: BRL → BRL transfer between two accounts of the same customer - value: - sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" - targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" - quoteId: "d4e5f6a7-b8c9-0123-defa-2345678901bc" - brlToUsd: - summary: BRL on source → USD on target (FX transfer) + brlTransfer: + summary: BRL transfer between two accounts of the same customer value: sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" - quoteId: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + amount: + value: "500.00" + asset: "BRL" responses: "201": description: Transfer accepted. The operation is in `REQUESTED` status. @@ -3045,8 +3055,8 @@ paths: schema: $ref: "#/components/schemas/OperationResponse" examples: - brlToUsdTransfer: - summary: BRL → USD transfer in REQUESTED state + brlTransfer: + summary: BRL transfer in REQUESTED state value: id: "4c6d1f10-5b4d-7e6d-cf5b-ae4d7e6dcf5b" customerId: "11111111-1111-1111-1111-111111111111" @@ -3054,12 +3064,12 @@ paths: id: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" owner: "Acme Importação Ltda" sourceAmount: - value: "5000.00" + value: "500.00" asset: "BRL" decimals: 2 targetAmount: - value: "1000.00" - asset: "USD" + value: "500.00" + asset: "BRL" decimals: 2 fees: [] transactions: [] @@ -3080,13 +3090,13 @@ paths: id: "b2c3d4e5-6f70-8901-bcde-f12345678901" owner: "Acme Importação Ltda" quote: - id: "e5f6a7b8-c9d0-1234-efab-3456789012cd" + id: "d4e5f6a7-b8c9-0123-defa-2345678901bc" effectiveRate: - value: "0.20" + value: "1.00" base: "BRL" - quote: "USD" + quote: "BRL" expiresAt: "2026-05-06T16:24:41Z" - consumedAt: null + consumedAt: "2026-05-06T16:24:11Z" createdAt: "2026-05-06T16:24:11Z" updatedAt: "2026-05-06T16:24:11Z" "400": @@ -3132,21 +3142,21 @@ paths: message: "'not-a-uuid' is not a valid UUID" field: "body:sourceAccountId" params: {} - missingQuoteId: - summary: quoteId is missing + missingAmount: + summary: amount is missing value: code: "INVALID_DATA" message: "Object contains invalid data" details: errors: - code: "REQUIRED" - message: "Parameter 'body:quoteId' not found in request" - field: "body:quoteId" + message: "Parameter 'body:amount' not found in request" + field: "body:amount" params: {} "401": $ref: "#/components/responses/UnauthorizedError" "404": - description: Source account, target account, or quote not found for this customer. + description: Source or target account not found for this customer. headers: X-Request-Id: $ref: "#/components/headers/RequestId" @@ -3159,8 +3169,6 @@ paths: $ref: "#/components/examples/AccountNotFound" targetAccountNotFound: $ref: "#/components/examples/TargetAccountNotFound" - quoteNotFound: - $ref: "#/components/examples/QuoteNotFound" "409": description: Idempotency key was reused with a different request body. headers: @@ -3199,25 +3207,21 @@ paths: targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" status: "OPENING" sourceAssetNotEnabled: - summary: Quote's source asset is not enabled on the source account + summary: Requested asset is not enabled on the source account value: code: "SOURCE_ASSET_NOT_ENABLED" - message: "Source asset is not enabled on the source account" + message: "Asset is not enabled on the source account" details: - quoteSourceAsset: "USD" + asset: "USD" sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890" targetAssetNotEnabled: - summary: Quote's target asset is not enabled on the target account + summary: Requested asset is not enabled on the target account value: code: "TARGET_ASSET_NOT_ENABLED" - message: "Target asset is not enabled on the target account" + message: "Asset is not enabled on the target account" details: - quoteTargetAsset: "USD" + asset: "USD" targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901" - quoteExpired: - $ref: "#/components/examples/QuoteExpired" - quoteAlreadyConsumed: - $ref: "#/components/examples/QuoteAlreadyConsumed" # ── Quotes ──────────────────────────────────────────────────────── /api/quotes: diff --git a/journeys/transfer.mdx b/journeys/transfer.mdx index 04cc64c..5c4efc6 100644 --- a/journeys/transfer.mdx +++ b/journeys/transfer.mdx @@ -5,38 +5,18 @@ description: "Step-by-step guide to moving funds between two accounts of the sam ## Overview -Transfers move funds between two accounts owned by the same customer, optionally converting assets in transit. Every transfer references a quote that locks the FX rate (or a 1:1 spot for same-asset). +Transfers move funds between two accounts owned by the same customer. They are same-asset — the same value is debited from the source account and credited to the target account, with no FX conversion. The system generates a 1:1 quote internally and returns it on the response; you do not need to create a quote first. To move between assets, [execute a swap](/journeys/swap) and then transfer. ## Prerequisites -- Two [active accounts](/journeys/open-multi-currency-account) owned by the same customer. The source account must hold the quote's source asset; the target account must have the quote's target asset enabled. +- Two [active accounts](/journeys/open-multi-currency-account) owned by the same customer, both with the requested asset enabled. The source must hold sufficient balance. - Valid [authentication credentials](/guides/authentication). ## Steps - - Quotes lock the FX rate (or a 1:1 spot for same-asset) for a short window and are bound to the source account. Provide either `sourceAmount` or `targetAmount`, never both. For a same-asset transfer set `sourceAsset` and `targetAsset` to the same value. - - ```bash - curl --request POST \ - --url {{sandboxUrl}}/api/quotes \ - --header 'Authorization: Bearer ' \ - --header 'X-Idempotency-Key: ' \ - --header 'Content-Type: application/json' \ - --data '{ - "accountId": "", - "sourceAsset": "BRL", - "targetAsset": "USD", - "sourceAmount": "5000.00" - }' - ``` - - The response returns the quote `id`, the locked `effectiveRate`, the computed `targetAmount`, and `expiresAt`. The quote can be consumed by exactly one operation before it expires. - - - Reference the source account, the target account, and the quote. Source and target must differ; the target account must be in `ACTIVE` status with the quote's target asset enabled. + Reference the source account, the target account, and the amount to move. Source and target must differ; the target account must be in `ACTIVE` status with the requested asset enabled. ```bash curl --request POST \ @@ -47,7 +27,10 @@ Transfers move funds between two accounts owned by the same customer, optionally --data '{ "sourceAccountId": "", "targetAccountId": "", - "quoteId": "" + "amount": { + "value": "500.00", + "asset": "BRL" + } }' ```