Send to an Account
Push funds to a specific bank account — payroll, vendor payments, refunds, or disbursements — using a single intent that works across institutions and networks.
Pushing money to a bank account — payroll, vendor payments, refunds, insurance claims, government disbursements — traditionally means building a different integration for each destination network. Domestic ACH has one message format, real-time payments have another, and cross-border wires have a third. Each channel has its own settlement timing, error handling, and reconciliation process.
With Minka, you describe the movement — source account, target account, amount, currency — and the ledger figures out how to execute it. The same intent works whether the target account is at the same bank, a different bank on the same network, or an institution in another country on a different rail. One API call, one settlement model, one reconciliation process.
What you're building
In this guide you'll create an intent that pushes funds from a hub bank's treasury account to a merchant's settlement account. The same pattern works for any push payment: payroll to an employee's savings account, a refund to a customer's checking account, or a disbursement to a beneficiary's mobile wallet.
Prerequisites
- SDK credentials and a connection to a ledger
- A funded source wallet (treasury, operating account, or disbursement pool)
- The target account address in
schema:number@domainformat — see About Wallets for addressing details
Send the payment
Create the intent
minka intent createThe CLI walks you through each field:
| Prompt | What to enter | Example |
|---|---|---|
| Handle | A unique payment reference | payout-2024-001234 |
| Source | The funding account | treasury@bank.co |
| Target | The receiving account | chk:98765@yourbank.co |
| Amount | Amount in smallest currency unit | 15000000 (150,000.00 COP) |
| Symbol | Currency handle | cop |
| Commit | Settlement mode | auto |
minka intent create submits a real payment. Use a test ledger for experimentation.
curl -X POST "https://{ledger-host}/v2/intents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {your-token}" \
-H "x-ledger: {your-ledger}" \
-d '{
"data": {
"handle": "payout-2024-001234",
"claims": [
{
"action": "transfer",
"amount": "15000000",
"symbol": { "handle": "cop" },
"source": {
"handle": "treasury@bank.co",
"custom": {
"name": "Banco Nacional S.A.",
"documentType": "nit",
"documentNumber": "860000000-1"
}
},
"target": {
"handle": "chk:98765@yourbank.co",
"custom": {
"name": "Melissa Ruiz",
"documentType": "cc",
"documentNumber": "814282133"
}
}
}
],
"config": { "commit": "auto" }
}
}'// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { response } = await sdk.intent
.init()
.data({
handle: 'payout-2024-001234',
claims: [
{
action: 'transfer',
amount: '15000000',
symbol: { handle: 'cop' },
source: {
handle: 'treasury@bank.co',
custom: {
name: 'Banco Nacional S.A.',
documentType: 'nit',
documentNumber: '860000000-1',
},
},
target: {
handle: 'chk:98765@yourbank.co',
custom: {
name: 'Melissa Ruiz',
documentType: 'cc',
documentNumber: '814282133',
},
},
},
],
config: { commit: 'auto' },
})
.hash()
.sign([{ keyPair }])
.send()import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
String json = """
{
"data": {
"handle": "payout-2024-001234",
"claims": [
{
"action": "transfer",
"amount": "15000000",
"symbol": { "handle": "cop" },
"source": { "handle": "treasury@bank.co" },
"target": { "handle": "chk:98765@yourbank.co" }
}
],
"config": { "commit": "auto" }
}
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://{ledger-host}/v2/intents"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer {your-token}")
.header("x-ledger", "{your-ledger}")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using System.Text;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer {your-token}");
client.DefaultRequestHeaders.Add("x-ledger", "{your-ledger}");
var json = """
{
"data": {
"handle": "payout-2024-001234",
"claims": [
{
"action": "transfer",
"amount": "15000000",
"symbol": { "handle": "cop" },
"source": { "handle": "treasury@bank.co" },
"target": { "handle": "chk:98765@yourbank.co" }
}
],
"config": { "commit": "auto" }
}
}
""";
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
"https://{ledger-host}/v2/intents", content);
var body = await response.Content.ReadAsStringAsync();The curl examples use a Bearer token for authentication. In production, every mutation also requires a cryptographic signature — see Sign Payments for the full signing flow. The TypeScript SDK handles signing automatically.
The source is the account the funds come from — a treasury wallet, an operating account, or a disbursement pool. The target is the destination bank account using the standard schema:number@domain addressing format. The claims array describes what should happen — each claim is a single instruction within the intent.
The custom object on source and target carries identity information — name, document type, and document number — for each party. The ledger passes these fields through to the bridge's debit and credit endpoints, so the receiving bank can identify who is sending and who is receiving.
The handle acts as an idempotency key. If a network glitch causes a retry, submitting the same handle again returns the original result instead of processing the payment twice. Use a meaningful reference: a payroll run ID, an invoice number, or a disbursement batch identifier.
Setting config.commit to "auto" tells the ledger to settle immediately once all participants confirm. Without it, the intent pauses at prepared and waits for a manual commit — useful for high-value payments that need approval.
Check the result
minka intent read payout-2024-001234curl "https://{ledger-host}/v2/intents/payout-2024-001234" \
-H "Authorization: Bearer {your-token}" \
-H "x-ledger: {your-ledger}"// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { intent } = await sdk.intent.read('payout-2024-001234')import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://{ledger-host}/v2/intents/payout-2024-001234"))
.header("Authorization", "Bearer {your-token}")
.header("x-ledger", "{your-ledger}")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer {your-token}");
client.DefaultRequestHeaders.Add("x-ledger", "{your-ledger}");
var response = await client.GetAsync(
"https://{ledger-host}/v2/intents/payout-2024-001234");
var body = await response.Content.ReadAsStringAsync();A completed status means the funds have settled — the source account was debited and the target account was credited atomically. If the status is rejected, the response includes the reason from the participant that couldn't complete.
{
"data": {
"handle": "payout-2024-001234",
"claims": [
{
"action": "transfer",
"amount": "15000000",
"symbol": { "handle": "cop" },
"source": {
"handle": "treasury@bank.co",
"custom": {
"name": "Banco Nacional S.A.",
"documentType": "nit",
"documentNumber": "860000000-1"
}
},
"target": {
"handle": "chk:98765@yourbank.co",
"custom": {
"name": "Melissa Ruiz",
"documentType": "cc",
"documentNumber": "814282133"
}
}
}
]
},
"meta": {
"status": "completed"
}
}Verify balances
Confirm the source was debited and the target was credited:
minka wallet read treasury@bank.co
minka wallet read chk:98765@yourbank.cocurl "https://{ledger-host}/v2/wallets/treasury@bank.co" \
-H "Authorization: Bearer {your-token}" \
-H "x-ledger: {your-ledger}"// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { balances: sourceBalances } = await sdk.wallet.getBalances('treasury@bank.co')
const { balances: targetBalances } = await sdk.wallet.getBalances('chk:98765@yourbank.co')import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://{ledger-host}/v2/wallets/treasury@bank.co"))
.header("Authorization", "Bearer {your-token}")
.header("x-ledger", "{your-ledger}")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());using System.Net.Http;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer {your-token}");
client.DefaultRequestHeaders.Add("x-ledger", "{your-ledger}");
var response = await client.GetAsync(
"https://{ledger-host}/v2/wallets/treasury@bank.co");
var body = await response.Content.ReadAsStringAsync();Both balances reflect the movement immediately. There is no delay, no pending state, no overnight reconciliation batch. The two-phase commit guarantees both sides updated atomically.
Routing across institutions
When the source and target are at different institutions — a hub bank paying out to a merchant's account at another bank — the ledger coordinates the cross-institutional movement automatically.
Same institution. If both wallets are native to the same ledger, the transfer is entirely internal. The ledger debits one wallet and credits the other in a single atomic operation. No bridge involved, no external calls.
Different institutions on the same network. If the target account is at a different bank connected through a bridge (e.g., ACH Colombia), the ledger calls the bridge to coordinate the external leg. The bridge translates the intent into the network's protocol, participates in the two-phase commit, and confirms when the destination bank has accepted the credit. Both the ledger balance and the external bank balance update together.
Different institutions on different networks. If the source bank is on one network and the target bank is on another, the ledger routes through the appropriate bridges for each side. The atomic settlement guarantee holds across both networks — if either side fails, both sides roll back.
From the API consumer's perspective, the intent looks identical in all three cases. The same fields, the same response format, the same lifecycle. The routing complexity stays inside the ledger.
Common payment patterns
The same intent structure works for any push payment scenario. The only things that change are the source, target, and amount.
| Pattern | Source | Target | When |
|---|---|---|---|
| Payroll | ops:payroll-pool@company.co | svgs:42424242@employee-bank.co | Monthly salary disbursement |
| Vendor payment | treasury@bank.co | chk:98765@yourbank.co | Merchant settlement, supplier invoices |
| Refund | ops:returns-pool@merchant.co | svgs:12345@customer-bank.co | Customer refund after QR payment |
| Government disbursement | treasury@gov-agency.co | svgs:67890@citizen-bank.co | Benefits, subsidies, tax refunds |
| Cross-border | treasury@bank.co | chk:98765@foreign-bank.us | International remittance |
What you learned
- How to push funds to a specific account using a single intent
- How the idempotency handle prevents double-processing on retries
- How the ledger routes payments across institutions and networks automatically
- How the two-phase commit guarantees atomic settlement even for cross-system payments
Common errors
| Error | Cause | Fix |
|---|---|---|
insufficient-funds | The source wallet does not have enough balance to cover the amount | Check the balance with GET /v2/wallets/{handle}/balances before submitting |
wallet-not-found | The source or target wallet handle does not exist on the ledger | Verify the wallet address format (schema:number@domain) and that the wallet has been created |
bridge-timeout | The external system connected via bridge did not respond within the timeout window | Retry with the same intent handle — idempotency ensures no duplicate processing |
Next steps
- Send a Payment — the introductory guide with account addressing basics
- Send a Payment to an Anchor — resolve a phone number or alias, then pay
- Onboard a Merchant — create the merchant wallet that receives these payments
- About Intents — how the two-phase commit protocol works under the hood
Create a Merchant
Create a merchant wallet with a complete business profile — name, location, tax regime, settlement account — so every payment artifact the merchant creates inherits the right data automatically.
Send to an Anchor
Resolve a phone number or alias to get account details, then create a payment intent — the two-step pattern for paying someone you know by identifier, not by account address.