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.

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

Adding a new merchant to a traditional payment system means filling out forms for every payment network the merchant needs to accept. QR codes require one set of fields, alias directories require another, card networks require a third. Each network has its own onboarding format, its own validation rules, and its own way of representing the same merchant.

With Minka, you create a single wallet that describes the merchant as a business entity — name, city, category, tax regime, settlement account. Every payment artifact the merchant creates afterward — QR codes, payment links, aliases — inherits this profile automatically. The bridge reads the wallet and translates it into whatever format the network requires. One onboarding, any network.

What you're building

In this guide you'll create a merchant wallet hierarchy — a parent merchant wallet that holds the full business profile and a store wallet that inherits from it. You'll also create a settlement account where the merchant's money lands. Once the merchant wallet exists, the merchant can create QR codes and other payment instruments without repeating their business details.

This guide uses a merchant as the example, but the same wallet creation pattern applies to any participant type. A bank creates an institution wallet with its own schema and regulatory fields. An individual creates a person wallet with identity details. The API call is identical — only the schema and custom fields change.

Prerequisites

  • SDK credentials and a connection to a ledger
  • A ledger with the cop symbol (or your currency) already created
  • The merchant-wallet and settlement-wallet schemas deployed on the ledger

Create the merchant wallet

Create the settlement account

The settlement account is where the merchant's money actually lands. It represents the merchant's real bank account on the ledger — a savings account, checking account, or deposit account at the hub bank. Settlement wallets are linked to the ACH bridge so that any intent involving them triggers two-phase commit with the external network.

minka wallet create

The CLI walks you through each field:

PromptWhat to enter
HandleThe account address, e.g. svgs:4242@bank.co
Schemasettlement-wallet
Bridgebridge@achcolombia.com.co
Custom{"name":"Starbucks Settlement","type":"settlement","bankCode":"007","accountType":"savings"}
curl -X POST "https://{ledger-host}/v2/wallets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {your-token}" \
  -H "x-ledger: {your-ledger}" \
  -d '{
    "data": {
      "handle": "svgs:4242@bank.co",
      "schema": "settlement-wallet",
      "bridge": "bridge@achcolombia.com.co",
      "custom": {
        "name": "Starbucks Settlement Account",
        "type": "settlement",
        "bankCode": "007",
        "accountType": "savings"
      }
    }
  }'
POST /v2/wallets
// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { response } = await sdk.wallet
  .init()
  .data({
    handle: 'svgs:4242@bank.co',
    schema: 'settlement-wallet',
    bridge: 'bridge@achcolombia.com.co',
    custom: {
      name: 'Starbucks Settlement Account',
      type: 'settlement',
      bankCode: '007',
      accountType: 'savings',
    },
  })
  .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": "svgs:4242@bank.co",
        "schema": "settlement-wallet",
        "bridge": "bridge@achcolombia.com.co",
        "custom": {
          "name": "Starbucks Settlement Account",
          "type": "settlement",
          "bankCode": "007",
          "accountType": "savings"
        }
      }
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://{ledger-host}/v2/wallets"))
    .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": "svgs:4242@bank.co",
        "schema": "settlement-wallet",
        "bridge": "bridge@achcolombia.com.co",
        "custom": {
          "name": "Starbucks Settlement Account",
          "type": "settlement",
          "bankCode": "007",
          "accountType": "savings"
        }
      }
    }
    """;

var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    "https://{ledger-host}/v2/wallets", 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 handle follows the standard addressing format: svgs for savings, 4242 for the account number, bank.co for the institution. The bridge field links this wallet to the ACH bridge — any intent debiting or crediting this wallet triggers two-phase commit with the external network, guaranteeing the ledger and the real bank account stay in sync.

Create the merchant wallet

The merchant wallet is the business identity. Its custom field holds the profile that bridges read when generating payment artifacts — QR payloads, alias registrations, network credentials. This is the parent wallet — it carries the full business profile that store wallets inherit from.

minka wallet create
PromptWhat to enter
HandleThe merchant identity, e.g. main@starbucks.com
Schemamerchant-wallet
CustomSee the profile fields below
curl -X POST "https://{ledger-host}/v2/wallets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {your-token}" \
  -H "x-ledger: {your-ledger}" \
  -d '{
    "data": {
      "handle": "main@starbucks.com",
      "schema": "merchant-wallet",
      "custom": {
        "name": "STARBUCKS COLOMBIA",
        "city": "BOGOTA",
        "taxId": "900123456-7",
        "category": "Fast food restaurant",
        "country": "CO",
        "postal": "110111",
        "taxRegime": "standard",
        "acquirer": "RBM",
        "settlement": "svgs:4242@bank.co",
        "type": "merchant"
      }
    }
  }'
POST /v2/wallets
// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { response } = await sdk.wallet
  .init()
  .data({
    handle: 'main@starbucks.com',
    schema: 'merchant-wallet',
    custom: {
      name: 'STARBUCKS COLOMBIA',
      city: 'BOGOTA',
      taxId: '900123456-7',
      category: 'Fast food restaurant',
      country: 'CO',
      postal: '110111',
      taxRegime: 'standard',
      acquirer: 'RBM',
      settlement: 'svgs:4242@bank.co',
      type: 'merchant',
    },
  })
  .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": "main@starbucks.com",
        "schema": "merchant-wallet",
        "custom": {
          "name": "STARBUCKS COLOMBIA",
          "city": "BOGOTA",
          "taxId": "900123456-7",
          "category": "Fast food restaurant",
          "country": "CO",
          "postal": "110111",
          "taxRegime": "standard",
          "acquirer": "RBM",
          "settlement": "svgs:4242@bank.co",
          "type": "merchant"
        }
      }
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://{ledger-host}/v2/wallets"))
    .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": "main@starbucks.com",
        "schema": "merchant-wallet",
        "custom": {
          "name": "STARBUCKS COLOMBIA",
          "city": "BOGOTA",
          "taxId": "900123456-7",
          "category": "Fast food restaurant",
          "country": "CO",
          "postal": "110111",
          "taxRegime": "standard",
          "acquirer": "RBM",
          "settlement": "svgs:4242@bank.co",
          "type": "merchant"
        }
      }
    }
    """;

var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    "https://{ledger-host}/v2/wallets", content);
var body = await response.Content.ReadAsStringAsync();

Each field in the profile serves a specific purpose:

FieldWhat it does
nameThe trading name shown to the person paying — appears on their bank app when they scan the QR. Maximum 25 characters.
cityThe city where this merchant operates. Used for display and geographic routing. Maximum 15 characters.
taxIdThe merchant's tax identification number — NIT in Colombia, CNPJ in Brazil, EIN in the US. The bridge derives the alias type automatically from this value (a 9-10 digit number with optional check digit is detected as NIT in Colombia).
categoryA human-readable description of the business ("coffee shop", "grocery store"). The bridge maps this to the correct 4-digit MCC using the EMVco profile's category table — the merchant never needs to look up MCC codes.
countryISO 3166-1 alpha-2 country code. Determines which EMVco profile the bridge uses — different countries have different acquirer GUIDs, currency codes, tax structures, and alias types.
postalPostal or ZIP code of the merchant's location. Some networks require it for geographic validation.
taxRegimeHow the merchant is taxed: standard (full rates), exempt (no taxes), simplified (flat rate), or special (country-specific rules). The bridge uses this to compute tax tags in the QR payload automatically — the merchant never manually provides tax breakdowns.
acquirerIdentifier of the acquiring institution on the payment network. Defaults from the EMVco profile if omitted.
settlementThe handle of the wallet where this merchant receives their money. When a QR payment arrives, the bridge reads this field to know where to credit the funds.
typeSet to merchant for the parent entity. Store wallets use store, kiosk, or online.

The merchant sets these fields once during onboarding. When they later create a QR code, the bridge reads this profile, maps the category to an MCC, detects the alias type from the taxId, selects the correct EMVco country profile, computes tax tags from the regime — and generates the complete network payload. The merchant never needs to know what EMVco Tag 52 means or how IVA condition codes work.

Create the store wallet

Store wallets carry only location-specific fields — name, city, postal code. Everything else (taxId, category, country, taxRegime, acquirer, settlement) is inherited from the parent merchant wallet. The bridge resolves inherited fields by reading main@{domain} when a store wallet lacks them — see domain for how domain-level inheritance works.

minka wallet create
PromptWhat to enter
HandleThe store address, e.g. store1@starbucks.com
Schemamerchant-wallet
Custom{"name":"STARBUCKS UNICENTRO","city":"BOGOTA","postal":"110111","type":"store"}
curl -X POST "https://{ledger-host}/v2/wallets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {your-token}" \
  -H "x-ledger: {your-ledger}" \
  -d '{
    "data": {
      "handle": "store1@starbucks.com",
      "schema": "merchant-wallet",
      "custom": {
        "name": "STARBUCKS UNICENTRO",
        "city": "BOGOTA",
        "postal": "110111",
        "type": "store"
      }
    }
  }'
POST /v2/wallets
// SDK setup: see How to Connect to a Ledger
// /moving-money/connect-to-ledger
const { response } = await sdk.wallet
  .init()
  .data({
    handle: 'store1@starbucks.com',
    schema: 'merchant-wallet',
    custom: {
      name: 'STARBUCKS UNICENTRO',
      city: 'BOGOTA',
      postal: '110111',
      type: 'store',
    },
  })
  .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": "store1@starbucks.com",
        "schema": "merchant-wallet",
        "custom": {
          "name": "STARBUCKS UNICENTRO",
          "city": "BOGOTA",
          "postal": "110111",
          "type": "store"
        }
      }
    }
    """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://{ledger-host}/v2/wallets"))
    .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": "store1@starbucks.com",
        "schema": "merchant-wallet",
        "custom": {
          "name": "STARBUCKS UNICENTRO",
          "city": "BOGOTA",
          "postal": "110111",
          "type": "store"
        }
      }
    }
    """;

var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync(
    "https://{ledger-host}/v2/wallets", content);
var body = await response.Content.ReadAsStringAsync();

Four fields instead of twelve. The store wallet is lean because the bridge does the heavy lifting — when it needs the taxId, acquirer, or settlement account, it reads the merchant wallet at main@starbucks.com in the same domain. Changing the acquirer or settlement account means updating one merchant wallet, not every store.

Verify the merchant profile

Confirm the merchant wallet was created with the correct profile:

minka wallet read main@starbucks.com
curl "https://{ledger-host}/v2/wallets/main@starbucks.com" \
  -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 { wallet } = await sdk.wallet.read('main@starbucks.com')
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/main@starbucks.com"))
    .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/main@starbucks.com");
var body = await response.Content.ReadAsStringAsync();

The response should include the complete custom object with all profile fields. Check that settlement points to the correct bank account wallet and that category matches the merchant's actual business activity — the bridge will map it to an MCC code.

Adding more stores

A merchant typically has one parent wallet (main@starbucks.com with type: merchant) and one store wallet per physical location (store1@starbucks.com, store2@starbucks.com, each with type: store). All stores share the same settlement account — money from every store flows to the same bank account.

Each store wallet carries only its own name, city, and postal code. Everything else is inherited from the merchant wallet. To add a new store, create a wallet with type: store in the same domain:

minka wallet create
# Handle: store2@starbucks.com
# Schema: merchant-wallet
# Custom: {"name":"STARBUCKS SALITRE","city":"BOGOTA","postal":"110221","type":"store"}

How onboarding connects to payment collection

Once the merchant wallet exists, the rest of the payment infrastructure builds on top of it:

QR codes. When you create an anchor pointing to a store wallet, the bridge reads the store's name and city, then walks up to the merchant wallet for the taxId, category, country, and settlement account. It generates the full EMVco payload — MCC resolved from category, alias type detected from taxId, tax tags computed from the regime — in a single API call.

Settlement. When an inbound payment arrives carrying a QR identifier, the bridge resolves the anchor to the store wallet, reads the merchant's settlement field, and creates a credit intent to svgs:4242@bank.co. The merchant's bank account is credited automatically.

New networks. When a new payment network connects to the ledger, existing merchant wallets work with it immediately. The bridge translates the same profile fields — name, city, category, tax regime — into whatever format the new network requires. No re-onboarding, no new forms.

What you learned

  • How to create a settlement account wallet linked to an external network bridge
  • How to create a merchant wallet with a full business profile
  • How to create lean store wallets that inherit from the merchant
  • How profile fields like taxRegime and category eliminate per-transaction complexity
  • How the wallet hierarchy enables one-time onboarding across any payment network

Error responses

When a request fails, the ledger returns a structured error with a code, message, and detail:

{
  "error": {
    "code": "schema-validation-failed",
    "message": "Required field 'city' is missing from custom object",
    "detail": {
      "schema": "merchant-wallet",
      "missing": ["city"]
    }
  }
}

Other how-to pages reference this format. The error codes below are the most common for each operation.

Common errors

ErrorCauseFix
schema-validation-failedA required field is missing from the custom object or has the wrong typeCheck the schema definition with minka schema read merchant-wallet to see required fields
handle-already-existsA wallet with this handle is already registered on the ledgerUse a unique handle, or query the existing wallet with GET /v2/wallets/{handle}
domain-not-foundThe domain in the wallet handle (e.g., starbucks.com) is not registeredRegister the domain first, or check for typos in the handle

Next steps

On this page