read this first
Documentation
Everything needed to take a shop from empty to selling, and to automate it afterwards.
Getting started
- Create an account and name your shop. The slug becomes your storefront address at
/s/<slug>; you can point your own domain at it later. - Add a wallet. Paste the account-level extended public key from your own wallet for each asset you want to accept. Check the three preview addresses against your wallet before you save.
- Add a product. Pick a delivery type, set a price, and, for serial products, paste your stock.
- Take a test order. On a development install with
CRYPTO_PROVIDER=mock, open/dev/payand settle your own invoice. The whole flow runs without touching a blockchain.
Wallets and xpubs
Shuz is non-custodial. It stores an extended public key and derives one receive address per invoice from it. An extended public key can generate addresses; it cannot sign a transaction. There is no private key on the server, which is why nobody running this software can move your money.
Export the account-level key, not the master key:
| Asset | Path | Prefix |
|---|---|---|
| Bitcoin | m/84'/0'/0' | zpub or xpub |
| Litecoin | m/84'/2'/0' | Mtub or Ltub |
| Ethereum, USDT, USDC | m/44'/60'/0' | xpub |
The three Ethereum-family assets share one key and one address per invoice, the watcher tells them apart by what actually arrived, not by where it arrived.
Never paste a key beginningxprv,zprvorLtpv. Those are private keys. Shuz refuses them, but no software can protect a key you have already pasted somewhere else.
Product types
| Type | What the buyer receives |
|---|---|
SERIAL | One entry from your stock pool per unit, reserved at checkout so it cannot be sold twice. |
FILE | A signed, expiring download link per attached file, with a per-order download cap. |
DYNAMIC | Whatever your own endpoint returns when we call it with a signed request. |
SERVICE | Nothing automatic, the line lands in your fulfilment queue with the buyer's details. |
INFO | A fixed text payload: an invite link, a template, a set of instructions. |
SUBSCRIPTION | Access for a period, renewed by a fresh invoice raised before it lapses. |
The order lifecycle
PENDING invoice issued, nothing seen on chain
PARTIAL some value arrived, below the tolerance floor
CONFIRMING enough value seen, waiting for confirmations
COMPLETED paid and every line delivered
AWAITING_FULFILMENT
paid, but a line needs you (a service, or a failed delivery)
EXPIRED never paid inside the window; stock released
REFUNDED you sent the coins back and recorded itFulfilment is per line, not per order. A basket holding a key and a bespoke service delivers the key immediately and leaves the service in your queue.
REST API
Create an API key under Shop → API keys. It is shown once. Authenticate with a bearer token:
curl https://your-shop.example/api/v1/products \
-H "Authorization: Bearer shuz_live_…"Keys are scoped (products:read, orders:write, and so on) and rate limited per key. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a rejected request carries Retry-After. The full schema is published at /api/v1/openapi.json.
Creating an order returns the payment address to show your buyer:
POST /api/v1/orders
{
"email": "buyer@example.com",
"asset": "BTC",
"lines": [{ "productId": "…", "quantity": 1 }]
}
201
{
"order": { "number": "4KQ7-M2XN-9F3T", "status": "PENDING" },
"invoice": { "address": "bc1q…", "expected": "0.00031312", "expiresAt": "…" }
}Webhooks
Add an endpoint under Shop → Webhooks and subscribe it to the events you care about. Each delivery carries:
X-Shuz-Event: order.completed
X-Shuz-Delivery: <delivery id>
X-Shuz-Timestamp: 1774000000
X-Shuz-Signature: <hex>The signature is HMAC-SHA256(timestamp + "." + rawBody) using your endpoint's secret. Verify it before trusting the body, and reject a timestamp more than a few minutes old so a captured request cannot be replayed:
import { createHmac, timingSafeEqual } from 'node:crypto'
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(signature))Failed deliveries are retried with an exponential backoff. An endpoint that fails ten times in a row is disabled automatically and you are told about it.
Licence validation
Enable licensing on a product and every unit sold issues a key. Your own software checks it against a public endpoint, no API key, because it runs on your customer's machine:
POST /api/v1/license/validate
{ "key": "A1B2C-D3E4F-G5H6J-K7L8M", "hardwareId": "…" }
{ "valid": true, "status": "ACTIVE",
"expiresAt": null, "activationsRemaining": 2 }Activations are bound to the hardware id you supply, counted against the product's limit, and revocable from the dashboard. A failed check never says which shop or product the key belongs to.