Multi-owner platforms — sub-merchants¶
On this page
What: within one merchant app, pay the money of many owners (machine owners, marketplace shops, franchisees) into each owner's own account — after each account has been approved by its holder in Gerege App. Time: ~2 hours (+ each owner's approval). You need: a merchant app and a live key (writes are live only), a Gerege Wallet account for each owner (a person's or an organisation's).
A platform (like vending.mn: every machine has a different owner) is one merchant app. Each owner is a sub-merchant: their own payout account, their own settlement stream, their own clearing; the rules (fees, schedule) are the app's.
1. Who owns what¶
Organisation (your workspace)
└─ Merchant app «Vending.mn» ── keys, webhook, review, fee rules
├─ Sub-merchant «Батаа ХХК» (code: owner-17) ── payout: ******4560 (approved by an admin of Батаа ХХК)
│ ├─ Terminal «Машин №7»
│ └─ Terminal «Машин №9»
└─ Sub-merchant «Сараа» (code: owner-42) ── payout: ******8812 (approved by Сараа herself)
└─ Terminal «Машин №12»
An intent takes its terminal's sub-merchant automatically, or names one with sub_merchant_id (for qr and
redirect without a terminal). The person sees the sub-merchant's display_name on the payment sheet.
2. Registering a sub-merchant¶
curl https://geregeapp.mn/api/v1/miniapp/sub-merchants -u "$GEREGE_LIVE_ID:$GEREGE_LIVE_SECRET" \
-H 'Content-Type: application/json' \
-d '{"code": "owner-17", "display_name": "Батаа ХХК", "legal_name": "Батаа Трейд ХХК", "reg_no": "1234567"}'
{
"id": "9a7c5d1e-…", "object": "sub_merchant", "code": "owner-17",
"display_name": "Батаа ХХК", "legal_name": "Батаа Трейд ХХК", "reg_no": "1234567", "status": "active",
"payouts_configured": false, "payout_account": null, "payout_linked_at": null,
"created_at": "2026-10-27T09:00:00Z", "updated_at": "2026-10-27T09:00:00Z"
}
code— your platform's owner ID (^[A-Za-z0-9_.:-]{1,40}$), a natural idempotency key: a repeatedPOST→ that row,200. It never changes (PATCH→400 code_immutable).display_name1–80 (shown to people),legal_name≤ 160,reg_no≤ 20.PATCH /sub-merchants/{id}— the names,reg_no,status: "active" | "disabled".GET /sub-merchants,GET …/{id}.- Writing is live key or developer console only (test →
403 live_only) — a sub-merchant is one record across both environments and decides where live captures go, so a sandbox secret must not be able to change it.
3. Linking a payout account — the owner approves it themselves¶
Your server cannot route an owner's income to an arbitrary account. If it could, the day your server was compromised would be a way to divert every owner's money elsewhere at once. So you make a request, and the account's holder approves it with Face ID in Gerege App:
curl https://geregeapp.mn/api/v1/miniapp/sub-merchants/9a7c5d1e-…/payout-link -u "$GEREGE_LIVE_ID:$GEREGE_LIVE_SECRET" \
-H 'Content-Type: application/json' -d '{"account": "MN75 0098 0009 8123 4560"}'
{
"id": "5f1e…", "object": "payout_link", "sub_merchant": "9a7c5d1e-…", "status": "pending",
"account_masked": "******4560", "requested_at": "2026-10-27T09:05:00Z", "expires_at": "2026-11-03T09:05:00Z",
"decided_at": null,
"terms": {
"mn": "Энэ данс руу орсон төлбөрүүдийн буцаалтыг (төлбөрөөс хойш 90 хоногийн дотор) энэ данснаас хасна.",
"en": "Refunds of payments made into this account (within 90 days of each payment) are taken back from this account."
}
}
account— an MN IBAN (MN+ 18 digits) or a 10-digit Gerege Wallet account number; spaces and hyphens are fine. The account must be active and in MNT (otherwise400 validation_error,path: "account"). A previouspendingrequest becomescanceled. ≤ 30 requests a minute (every request sends a push to a person).- Who is asked is the account's holder as registered in the ledger:
| Account | Who approves |
|---|---|
| A person's Gerege Wallet | that person themselves |
| An organisation's Gerege Wallet account | all current admins of that organisation are notified; any one of them approves (whether they are still an admin is checked again at the moment of approval) |
| Neither a Gerege App person nor an organisation | 422 account_unsupported |
No admin in the organisation → 422 no_admin; the admins could not be determined for the moment →
503 directory_unavailable (this is not a "no" — try again).
3. The holder (admin) gets a notification: «Данс холбох хүсэлт» ("Account link request") — its text is only your
app's approved name (never text you wrote — someone's words on a lock screen are a phishing channel). Opening
the request shows the app, the sub-merchant's name/legal name/registration number, the masked account and the
terms next to the button → Face ID → approve or decline (Gerege App: Тохиргоо → Хэтэвч → «Худалдагч ба
байгууллага» (Settings → Wallet → "Merchants and organisations"); the API, GET /api/v1/miniapp/me/payout-links,
POST …/{id}/approve · …/decline, is ready).
4. terms — what approving means: while settlements are off (off/shadow — production is shadow now), the
sub-merchant's payments go straight into this account and their refunds (within 90 days) come out of this
account. With settlements on, refunds come out of clearing first. The holder must have known and agreed to
this — so the request, the notification, the approval screen and your response (terms) all four say it. Show it
to the owner in your own UI too.
5. Once approved, the sub-merchant has payouts_configured: true, payout_account: "******4560", and the previous
approved link becomes superseded.
States: pending | approved | declined | canceled | superseded | expired (expired = not answered within 7 days).
GET /sub-merchants/{id}/payout-links — the last 50, newest first. If, when the holder approves, the account is no
longer theirs/no longer active, 409 account_unavailable; if core banking did not answer, 502 core_unavailable
(never a "no").
4. Taking payments¶
- A terminal intent: omit
sub_merchant_id— it is the terminal's. Giving a different sub-merchant is400 validation_error. qr/redirect:"sub_merchant_id": "9a7c…".- A live intent is not created while the sub-merchant's payout is not approved:
409 payouts_not_configured— so that nobody is shown a payment sheet they cannot pay. A test intent needs no payout. - A disabled sub-merchant →
409 sub_merchant_disabled.
Do not disable a sub-merchant while money is on hold
A disabled sub-merchant takes no new money — capturing an intent in requires_capture gives
409 payouts_not_configured. First capture or cancel that sub-merchant's intents on hold, then set it to
disabled.
5. Settlements per sub-merchant¶
| Settlement mode (operator) | Captured money | Refunds |
|---|---|---|
off / shadow (the default; production is shadow now) |
straight into the sub-merchant's approved payout account | from that account (terms) |
on + switched on for the app |
into the sub-merchant's clearing; fees by the app's rule; to the payout account on schedule | from clearing first |
- Settlements are separate for each (app, sub-merchant) stream: its own sequence, carry, shortfall. The app's own
stream (
sub_merchant: null) is the intents without a sub-merchant. GET /settlements,GET /settlements/{id}(live) and theSettlementobject of thesettlement.paidwebhook carry"sub_merchant": "<id>" | null— break your reports down per owner with it.- The fee formula and carry — Mini app payments §7.
- A CSV report broken down by sub-merchant — «coming soon» (dashboard).
6. Owners changing and leaving¶
- Changing the payout account — a new
payout-linkrequest; when the holder approves, the old one becomessuperseded. Until then the old account stays. - A machine to a new owner — a new sub-merchant (a new
code), get its payout approved, thenPATCH /terminals/{id} {"sub_merchant_id": "<new>"}. Money already captured for the previous owner (and money in settlement) stays theirs. - An owner leaves — finish their intents on hold (§4), then
PATCH /sub-merchants/{id} {"status": "disabled"}. The row is not deleted — it is needed for history and refunds.
7. Code¶
Node (@gerege/miniapp-node):
const owner = await gerege.subMerchants.create({ code: "owner-17", display_name: "Батаа ХХК", reg_no: "1234567" });
const link = await gerege.subMerchants.requestPayoutLink(owner.id, { account: "MN75 0098 0009 8123 4560" });
showToOwner(link.status, link.expires_at); // the owner approves in Gerege App; show the terms in your own UI too
// Later (a cron, or when the owner's dashboard opens):
const current = await gerege.subMerchants.retrieve(owner.id);
if (current.payouts_configured) enableMachines(owner.id);
Go (sdk/go):
owner, err := c.SubMerchants.Create(ctx, miniapp.CreateSubMerchantParams{Code: "owner-17", DisplayName: "Батаа ХХК"})
link, err := c.SubMerchants.RequestPayoutLink(ctx, owner.ID, "MN750098000981234560")
links, err := c.SubMerchants.ListPayoutLinks(ctx, owner.ID) // pending → approved
The current SDK's PayoutLink type does not carry the terms field yet (it is in the JSON) — it will be updated.
8. Implementation status¶
| Part | Status |
|---|---|
Sub-merchants, payout-link requests and terms, person/organisation accounts, (app, sub-merchant) settlements |
✓ |
Gerege App approval screen (Face ID, terms next to the button) |
✓ (iOS) |
| Reports broken down by sub-merchant, dashboard screens | «coming soon» |