Skip to main content

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.