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.

Audience: Developers, Integration Engineers · Read time: 4 min

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@domain format — see About Wallets for addressing details

Send the payment

Create the intent

minka intent create

The CLI walks you through each field:

PromptWhat to enterExample
HandleA unique payment referencepayout-2024-001234
SourceThe funding accounttreasury@bank.co
TargetThe receiving accountchk:98765@yourbank.co
AmountAmount in smallest currency unit15000000 (150,000.00 COP)
SymbolCurrency handlecop
CommitSettlement modeauto

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" }
    }
  }'
POST /v2/intents
// 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-001234
curl "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.co
curl "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.

PatternSourceTargetWhen
Payrollops:payroll-pool@company.cosvgs:42424242@employee-bank.coMonthly salary disbursement
Vendor paymenttreasury@bank.cochk:98765@yourbank.coMerchant settlement, supplier invoices
Refundops:returns-pool@merchant.cosvgs:12345@customer-bank.coCustomer refund after QR payment
Government disbursementtreasury@gov-agency.cosvgs:67890@citizen-bank.coBenefits, subsidies, tax refunds
Cross-bordertreasury@bank.cochk:98765@foreign-bank.usInternational 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

ErrorCauseFix
insufficient-fundsThe source wallet does not have enough balance to cover the amountCheck the balance with GET /v2/wallets/{handle}/balances before submitting
wallet-not-foundThe source or target wallet handle does not exist on the ledgerVerify the wallet address format (schema:number@domain) and that the wallet has been created
bridge-timeoutThe external system connected via bridge did not respond within the timeout windowRetry with the same intent handle — idempotency ensures no duplicate processing

Next steps

On this page