Send a Payment
Move money between two accounts with a single API call — the same instruction works regardless of the payment network, currency, or country.
Sending a payment through a traditional payment network means learning that network's message format, handling its specific error codes, building its settlement flow, and reconciling its transactions separately from every other rail you support. Each new network is a new integration project.
With Minka, you describe what should happen — move this amount from this account to that account — and the ledger handles everything else. One API call, one format, one settlement model. The same instruction works whether the money moves between two accounts at the same bank, across institutions through a national switch, or across borders through a card network.
This guide walks you through submitting your first payment.
How accounts are addressed
Every account in Minka is identified by a structured address:
schema:number@domainSchema describes the account type — svgs for savings, chk for checking, loan for loans, and so on. Number is the account identifier at the institution. Domain is the institution's domain name — globally unique, no ambiguity between banks in different countries.
For example, svgs:4242@bank.co refers to savings account 4242 at bank.co — a merchant's settlement account at the hub bank. And treasury@bank.co is the hub's internal reserve wallet used for funding payouts.
This addressing scheme gives the ledger everything it needs to route a payment: what kind of account, which specific account, and which institution holds it. See About Wallets for a deeper look at how addressing and resolution work.
Submit the payment
Create the intent
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": "payment-001",
"claims": [
{
"action": "transfer",
"amount": 1500000,
"symbol": { "handle": "cop" },
"source": { "handle": "treasury@bank.co" },
"target": { "handle": "svgs:4242@bank.co" }
}
],
"config": { "commit": "auto" }
}
}'That single claim says: transfer 1500000 base units (15,000.00 COP) of the cop symbol from the hub treasury to a merchant's settlement account. Amounts are always integers in the symbol's smallest unit — centavos for COP, cents for USD. The cop symbol has a factor of 100, so 1500000 ÷ 100 = 15,000.00.
The handle is your chosen identifier for this payment. It 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. Choose something meaningful: an invoice number, a transaction reference, or a timestamp-based identifier.
Setting config.commit to "auto" tells the ledger to settle the payment as soon as every participant has confirmed. Without it, the intent pauses at prepared status and waits for a manual commit call — useful when you need human approval before finalizing high-value transfers.
Check the result
Query the intent by its handle to see where it landed:
curl "https://{ledger-host}/v2/intents/payment-001" \
-H "Authorization: Bearer {your-token}" \
-H "x-ledger: {your-ledger}"A completed status means the payment has settled — both sides are final, balances are updated, and the books are in sync. A rejected status means something failed — the response includes the reason from the participant that couldn't complete its part:
{
"meta": {
"status": "rejected",
"reason": "insufficient-funds",
"detail": "Source wallet treasury@bank.co has insufficient balance for 1500000 cop"
}
}Common rejection reasons include insufficient-funds, wallet-not-found, policy-rejected, and bridge-timeout. The detail field provides a human-readable explanation specific to the failure.
There is no ambiguous middle state. The payment either completed everywhere or was rolled back everywhere. This is the atomic guarantee that eliminates reconciliation work.
What happened behind the scenes
When you submitted that intent, the ledger handled several steps on your behalf — steps that would traditionally require custom integration code for each payment network involved.
Address resolution. The ledger resolved both account addresses — treasury@bank.co (a native wallet) and svgs:4242@bank.co (a bridge wallet linked to the ACH network).
Routing. Since the target is a bridge wallet linked to bridge@achcolombia.com.co, the ledger called the ACH bridge to coordinate the external leg. The bridge translates the intent into the network's protocol and confirms when the destination bank has accepted the credit.
Atomic settlement. The ledger coordinated a two-phase commit. It asked every participant to prepare and reserve funds first, then committed only after every party confirmed. If any participant couldn't complete — a timeout, a validation error, insufficient funds — the entire payment was rolled back across all systems. No money in limbo, no "cleared on one side, pending on the other."
From your side, the API call was identical regardless of whether the payment moved within one bank or across networks. One instruction, one response format, one lifecycle.
What you learned
- How to address accounts using the
schema:number@domainformat - How to submit a payment intent with a single claim
- How to verify the result and interpret the intent lifecycle
- How the ledger handles routing and atomic settlement behind the scenes
What's next
- Collect a Payment — set up a QR code so customers can pay your store
- About Intents — deeper look at how intents orchestrate payments across any network
Create a Wallet
Set up a merchant store wallet on the ledger — with automatic balance management, multi-currency support, and full visibility into every payment your merchant receives.
Collect a Payment
Generate a QR code at checkout and start receiving payments from any connected network — one API call to create the code, one scan to collect the money.