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..93b6a00 100644
--- a/apis/fx-payment/openapi.yml
+++ b/apis/fx-payment/openapi.yml
@@ -973,6 +973,49 @@ components:
example: "PIX_KEY"
required:
- rail
+ CreateTransferRequest:
+ type: object
+ description: >
+ 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.
+ 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 `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: >
+ 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
+ - amount
WithdrawalBeneficiary:
description: |
Beneficiary for the withdrawal. Discriminated by `type`.
@@ -1088,8 +1131,10 @@ 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`. 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
@@ -1222,12 +1267,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 +1317,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 +2286,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 +3009,220 @@ paths:
quoteAlreadyConsumed:
$ref: "#/components/examples/QuoteAlreadyConsumed"
+ # ── Operations: Transfers ─────────────────────────────────────────
+ /api/operations/transfer:
+ post:
+ operationId: createTransfer
+ tags:
+ - Operations
+ summary: Create a transfer
+ description: >
+ 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.
+ parameters:
+ - $ref: "#/components/parameters/IdempotencyKey"
+ - $ref: "#/components/parameters/TraceVersion"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateTransferRequest"
+ examples:
+ brlTransfer:
+ summary: BRL transfer between two accounts of the same customer
+ value:
+ sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890"
+ targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901"
+ amount:
+ value: "500.00"
+ asset: "BRL"
+ 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:
+ brlTransfer:
+ summary: BRL 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: "500.00"
+ asset: "BRL"
+ decimals: 2
+ targetAmount:
+ value: "500.00"
+ asset: "BRL"
+ 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: "d4e5f6a7-b8c9-0123-defa-2345678901bc"
+ effectiveRate:
+ value: "1.00"
+ base: "BRL"
+ quote: "BRL"
+ expiresAt: "2026-05-06T16:24:41Z"
+ consumedAt: "2026-05-06T16:24:11Z"
+ 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: {}
+ missingAmount:
+ summary: amount is missing
+ value:
+ code: "INVALID_DATA"
+ message: "Object contains invalid data"
+ details:
+ errors:
+ - code: "REQUIRED"
+ message: "Parameter 'body:amount' not found in request"
+ field: "body:amount"
+ params: {}
+ "401":
+ $ref: "#/components/responses/UnauthorizedError"
+ "404":
+ description: Source or target account 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"
+ "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: Requested asset is not enabled on the source account
+ value:
+ code: "SOURCE_ASSET_NOT_ENABLED"
+ message: "Asset is not enabled on the source account"
+ details:
+ asset: "USD"
+ sourceAccountId: "a1b2c3d4-5e6f-7890-abcd-ef1234567890"
+ targetAssetNotEnabled:
+ summary: Requested asset is not enabled on the target account
+ value:
+ code: "TARGET_ASSET_NOT_ENABLED"
+ message: "Asset is not enabled on the target account"
+ details:
+ asset: "USD"
+ targetAccountId: "b2c3d4e5-6f70-8901-bcde-f12345678901"
+
# ── 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..5c4efc6
--- /dev/null
+++ b/journeys/transfer.mdx
@@ -0,0 +1,58 @@
+---
+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. 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, both with the requested asset enabled. The source must hold sufficient balance.
+- Valid [authentication credentials](/guides/authentication).
+
+## Steps
+
+
+
+ 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 \
+ --url {{sandboxUrl}}/api/operations/transfer \
+ --header 'Authorization: Bearer ' \
+ --header 'X-Idempotency-Key: ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "sourceAccountId": "",
+ "targetAccountId": "",
+ "amount": {
+ "value": "500.00",
+ "asset": "BRL"
+ }
+ }'
+ ```
+
+ 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.