Sub-merchants
If you are a fintech reselling payment acceptance, your merchants are shops that will never run an Edge container and will never hold a ChainOS account. This is how you take payments for them.
What a merchant is, and is not
A merchant is a name a payer sees, a logo, a commission and a balance. It is not a
tenant: no users, no login, no credentials of its own, no row in accounts. Your own
product is their dashboard.
The custody position, in plain terms
Read this paragraph before you build anything else.
Ziklag still cannot move funds. Payment addresses derive from your extended public
key and sweep to the address your Edge re-derives at M/1/1 — the only destination
its signing guard admits.
But a sub-merchant's money lands in your treasury. That makes you the custodian of your merchants' funds, and what a merchant is owed becomes a book entry rather than a balance sitting somewhere addressable. ChainOS keeps that book for you and can prove it adds up. It cannot pay anybody.
That is a licensing position, not an implementation detail. If it is not the model you want, the alternative is for each merchant to hold their own ChainOS account and run their own Edge — in which case they are a customer of ours, not of yours, and none of this page applies.
Provisioning
curl -X POST $CHAINOS/v1/payments/merchants \
-H "X-API-Key: $CHAINOS_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "corner-shop",
"externalRef": "MERCHANT-4417",
"displayName": "Corner Shop",
"supportEmail": "help@corner-shop.example",
"feeBps": 250
}'
Two of those fields are identity and cannot be changed afterwards.
slug appears in payment-page URLs, which you may already have sent to customers.
externalRef is your own identifier for this merchant, and it is worth setting on
every one. Without it, reconciling your book against ours is a name match. It is unique
per account, so provisioning the same merchant twice is refused rather than producing two
merchants that each accumulate half a balance — which nothing would report, because both
would be real.
feeBps is your commission in basis points. It is applied at capture as a second entry
beside the credit, so a merchant's statement shows what they took and what it cost them
as two lines. Changing it is not retroactive.
Taking a payment for a merchant
Name the merchant when you create the checkout:
{ "chain": "bsc", "asset": "USDT", "amount": "100.00", "merchantId": "…" }
Everything downstream follows from that one field. The session, its receipts, its
invoice and every payment.* webhook carry merchantId, and the branding on the page
is resolved from it.
Giving a merchant their own key
Mint a key pair bound to one merchant:
POST /v1/payments/keys
{ "label": "Corner Shop storefront", "merchantId": "…",
"origins": ["https://corner-shop.example"], "maxAmount": "500000000" }
Such a key may act for that merchant and for nothing else. It cannot list your merchants,
create one, read another's balance, or record a payout. It may omit merchantId when
creating a checkout and have its own filled in; naming a different one is refused.
The scope cannot be changed later. Re-pointing a key at another merchant would start crediting one merchant's payments to another, using a credential somebody else already holds — so rotate mints a successor with the same scope, and there is no endpoint that moves it.
Suspending a merchant
PATCH /v1/payments/merchants/{id}/status
{ "status": "suspended" }
Suspension stops new checkouts and nothing else. Every session already open stays payable to the end of its monitoring window. That is deliberate: a payer who broadcast a transaction ninety seconds before you suspended their merchant has sent real money, and something still has to credit it. Refusing it would strand those funds at an address nothing reconciles.
There is no delete. A merchant that has taken a payment cannot be removed — the receipts
that say what it was owed outlive it, because a merchant disputing a final balance needs
those rows more than you need a tidy table. Use closed.
Co-branding their checkout
Upload their logo and yours:
curl -X POST $CHAINOS/v1/payments/merchants/$MERCHANT/brand \
-H "X-API-Key: $CHAINOS_KEY" -F file=@corner-shop.png
curl -X POST $CHAINOS/v1/payments/brand \
-H "X-API-Key: $CHAINOS_KEY" -F file=@your-logo.png
PNG, JPEG or WebP, 256 KB or smaller. The merchant's mark leads on the page and yours sits beside it as "Payments by …", because a payer knows the shop and has usually never heard of you.
SVG is refused, and the type is read from the bytes rather than from the
Content-Type you send, so relabelling one does not get it in. An SVG is a document that
can carry script, and this file is served from the same origin as the page displaying a
payment address.
Logos are uploaded rather than linked for the same reason. A URL you supplied would need
img-src widened to arbitrary hosts on that page, and would leave whoever controls that
host able to change what a payer sees at the moment they decide whether the page is real.
What a payer sees, and does not
The checkout carries the merchant's name and logo, yours, and support contacts. It
carries no identifiers — not merchantId, not accountId, nothing internal. That is
enforced by a test that walks the shape of the payer-facing records and fails the build on
a UUID, rather than by anybody remembering.