Skip to main content

Internal Transfers Guide

This guide explains how to perform internal transfers between accounts at the same bank, covering the three available methods and their use cases.

Overview

Internal transfers move funds between accounts within the same bank, without going through the PIX network. They are processed instantly.

There are three ways to identify the destination account:

MethodEndpointDestination requirementUse case
By Account ID/transfers/internalMust have API access enabledTransfers between your own accounts
By Document/transfers/internal/by-documentAny account at the bankTransfers to any account holder
By Branch/Account/transfers/internal/by-bank-accountMust have API access enabledWhen you have branch and account number

1. Transfer by Account ID

The most performant method. Use when both source and destination accounts are registered in the API.

Endpoint: POST /v1/accounts/{accountId}/transfers/internal

curl -X POST "https://tenant.api.corpx.com/v1/accounts/{accountId}/transfers/internal" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: tenant-yourcompany" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: int-001" \
-d '{
"destinationAccountId": "773107de-139e-48d1-9462-f4e88f251891",
"value": 1500.00,
"description": "Transfer between branches",
"identifier": "branch-transfer-001"
}'

Fields:

FieldTypeRequiredDescription
destinationAccountIdstring (UUID)YesDestination account UUID (API internal ID)
valuenumberYesAmount in BRL (max 2 decimal places)
descriptionstringNoTransfer description (max 140 characters)
identifierstringNoUnique identifier for tracking. Auto-generated when omitted

Response (200):

{
"paymentId": "pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"workflowId": "internal-out-{accountId}-branch-transfer-001-int-001",
"runId": "b7c1f0e2-...",
"transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"endToEndId": "E50871921202603181530000000001",
"destinationAccountId": "773107de-139e-48d1-9462-f4e88f251891",
"status": "COMPLETED",
"value": 1500.00,
"description": "Transfer between branches",
"identifier": "branch-transfer-001",
"idempotencyKey": "int-001",
"completedAt": "2026-03-18T15:30:00Z"
}

When the transfer is refused, the same body comes back with 422, status: "FAILED" and the errorCode/errorReason fields filled in.

2. Transfer by Document (CPF/CNPJ)

Use when the destination account is not registered in the API. Works for any account in the banking ecosystem.

Endpoint: POST /v1/accounts/{accountId}/transfers/internal/by-document

curl -X POST "https://tenant.api.corpx.com/v1/accounts/{accountId}/transfers/internal/by-document" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: tenant-yourcompany" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: int-002" \
-d '{
"document": "12345678000190",
"value": 500.00,
"description": "Supplier payment",
"identifier": "supplier-payment-002"
}'

Fields:

FieldTypeRequiredDescription
documentstringYesCPF or CNPJ of the destination account holder (numbers only)
valuenumberYesAmount in BRL
descriptionstringNoTransfer description (max 140 characters)
identifierstringNoUnique identifier for tracking. Auto-generated when omitted
Document with multiple accounts

When the destination document has more than one active account, addressing by document is ambiguous and the request is rejected with 409 multiple_destination_accounts. In that case, use /transfers/internal/by-bank-account (branch + account number) or /transfers/internal (destinationAccountId).

3. Transfer by Branch and Account Number

Use when you have the branch and account number. Both accounts must be registered in the API.

Endpoint: POST /v1/accounts/{accountId}/transfers/internal/by-bank-account

curl -X POST "https://tenant.api.corpx.com/v1/accounts/{accountId}/transfers/internal/by-bank-account" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: tenant-yourcompany" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: int-003" \
-d '{
"branch": "0001",
"accountNumber": "200038274",
"holderDocument": "12345678000190",
"holderName": "DESTINATION COMPANY LTDA",
"value": 250.00,
"description": "Refund",
"identifier": "refund-003"
}'

Fields:

FieldTypeRequiredDescription
branchstringYesDestination account branch
accountNumberstringYesDestination account number
holderDocumentstringYesCPF/CNPJ of the destination account holder — the settlement bank requires it for routing
holderNamestringNoDestination holder name
valuenumberYesAmount in BRL
descriptionstringNoTransfer description (max 140 characters)
identifierstringNoUnique identifier for tracking. Auto-generated when omitted

Which method to choose?

Does the destination account have API access?
├── YES → Do you have the Account ID (UUID)?
│ ├── YES → Use /transfers/internal (fastest)
│ └── NO → Use /transfers/internal/by-bank-account
└── NO → Use /transfers/internal/by-document

Practical examples:

  • Moving funds between your company's branches/transfers/internal (by Account ID)
  • Paying a supplier at the bank/transfers/internal/by-document (by CPF/CNPJ)
  • Transferring to a known account by branch number/transfers/internal/by-bank-account

Webhooks

After an internal transfer, both parties receive a webhook:

  • Sender: transfer.internal.out
  • Receiver: transfer.internal.in
{
"id": "transfer-internal-out-pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"type": "transfer.internal.out",
"occurredAt": "2026-03-18T15:30:00.000000000Z",
"schemaVersion": "1.0",
"environment": "production",
"tenantId": "tenant-yourcompany",
"accountId": "a1b2c3d4-aaaa-bbbb-cccc-111122223333",
"data": {
"paymentId": "pay_2f4a0f88-2147-49f2-a4e2-4f7b9f6c0f7a",
"transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"endToEnd": "E50871921202603181530000000001",
"accountId": "a1b2c3d4-aaaa-bbbb-cccc-111122223333",
"direction": "OUT",
"sourceAccountId": "a1b2c3d4-aaaa-bbbb-cccc-111122223333",
"sourceTenantId": "tenant-yourcompany",
"destinationAccountId": "773107de-139e-48d1-9462-f4e88f251891",
"destinationTenantId": "tenant-yourcompany",
"amount": 1500.00,
"description": "Transfer to SP branch",
"identifier": "branch-transfer-001",
"status": "SUCCESS"
}
}

The data block is flat: there are no nested source/destination objects with name and tax ID — the counterparties show up as sourceAccountId / destinationAccountId. The transfer.internal.in leg carries the same payload with the destination accountId and direction: "IN".

Status on internal transfers

Because settlement is atomic and synchronous, the webhook always arrives with data.status: "SUCCESS" — it is the only possible value for transfer.internal.*. Failures (insufficient funds, non-existent destination account, blocked account, etc.) are signalled in the HTTP response of the POST /transfers/internal* call itself and never produce a webhook.

Guaranteed reconciliation fields

The webhook sent to the payer (transfer.internal.out) carries the same identifier and description you sent in the request body (POST /transfers/internal*), together with the paymentId and transactionId that are also in the synchronous response. That removes the need for extra statement lookups to close the transfer on your side.

The transfer.internal.in leg is only emitted when the destination account is also managed at CorpX; it repeats the same fields (including the identifier set by the payer), changing only accountId and direction.

To receive these webhooks, include transfer.internal.in and/or transfer.internal.out in your subscription event types.

Common Errors

HTTPerrorCodeCause
400missing_fieldsMissing required field (value, description, destination)
400invalid_fieldMore than 2 decimal places, invalid branch, malformed description
400invalid_identifieridentifier outside the [A-Za-z0-9._-] charset or longer than 38 characters
404beneficiary_not_foundDestination account does not exist (unknown destinationAccountId at CorpX, or a document with no account at the settlement bank)
422beneficiary_not_active_at_partnerAccount exists at CorpX, but the settlement bank has no holder yet (accreditation pending)
422beneficiary_incompleteSettlement bank returned a holder without branch/account
422recipient_account_not_foundSettlement bank could not find the destination account provided
409multiple_destination_accountsDocument has more than one active account (use by-bank-account)
409identifier_conflictA transfer with this identifier already exists for this source account
422insufficient_fundsAvailable balance does not cover the amount
422balance_reservation_failedBalance exists but could not be reserved (hold, concurrency)
422partner_account_disabledSource account disabled at the bank
422limit_exceeded_transaction / limit_exceeded_dailyTransfer limit exceeded
422partner_rejectedRefused by the bank's internal policy. The partner block carries the stated reason
502partner_error / partner_unavailableError or outage at the bank
422 is an outcome, 202 is a doubt

A refused transfer answers 422 with status: "FAILED", errorCode and errorReason — the money did not move and no webhook is coming. A 202 with status: "PENDING" means something else: the call went out and confirmation did not arrive within the synchronous window. In that case check the statement before re-issuing — repeating the POST with the same Idempotency-Key reuses the in-flight operation, but a fresh key may transfer twice.