Billing is handled by Dodo Payments, a merchant-of-record that
settles sales tax and VAT on your behalf. The runtime reads its
config from environment variables in
/etc/qnt/qnt-server.env — set a key, two product IDs,
and a webhook secret, and the upgrade flow comes alive. This whole
section is optional: skip it if you don't charge your users.
21tunnel bills through Dodo Payments — it replaced Stripe. Dodo is a merchant-of-record, so it collects and remits sales tax and VAT for you rather than leaving that to your own accounting.
Billing is entirely optional. If you run 21tunnel for your own team and don't charge anyone, leave it unconfigured and move on. You only need this page if you want to sell the Pro and Team plans to your users.
Plans gate features. Custom domains are one example: Free allows 0, Pro allows 1, and Team is unlimited. The entitlement follows the user's active plan.
Creating products, finding API keys, and registering webhooks all happen inside the Dodo dashboard. Those screens change, so this guide points you at the values 21tunnel reads rather than re-drawing Dodo's UI. For the dashboard steps themselves, follow Dodo Payments' own documentation.
Billing config is read from environment variables, not from
server.toml. They live in
/etc/qnt/qnt-server.env, which the systemd unit loads
via EnvironmentFile=-/etc/qnt/qnt-server.env.
QNT_DODO_API_KEY — your Dodo API key. A test
key keeps billing in test mode; a live key is for production.QNT_DODO_WEBHOOK_SECRET — the webhook signing
secret from the Dodo dashboard, used to verify inbound
webhooks.QNT_DODO_PRODUCT_PRO — the Dodo product ID for
the Pro plan.QNT_DODO_PRODUCT_TEAM — the Dodo product ID for
the Team plan.QNT_DODO_BASE_URL — the Dodo API base:
https://test.dodopayments.com for test mode,
https://live.dodopayments.com for live mode.
Drop these lines into /etc/qnt/qnt-server.env with
your own values. The base URL must match the mode of your key —
test key with the test base, live key with the live base.
QNT_DODO_API_KEY=...
QNT_DODO_BASE_URL=https://test.dodopayments.com
QNT_DODO_PRODUCT_PRO=prod_...
QNT_DODO_PRODUCT_TEAM=prod_...
QNT_DODO_WEBHOOK_SECRET=whsec_...
Switch to live by swapping in your live key and setting
QNT_DODO_BASE_URL=https://live.dodopayments.com.
Live products and a live webhook secret are distinct from their
test-mode counterparts.
Each paid plan maps to one Dodo product. You create the products
in the Dodo dashboard, then copy their IDs into the
QNT_DODO_PRODUCT_* variables.
In the Dodo dashboard, create a product for the Pro plan and a product for the Team plan, following Dodo's own docs for the product-creation flow and pricing setup. Each product gets a unique product ID.
QNT_DODO_PRODUCT_PRO.QNT_DODO_PRODUCT_TEAM.The customer portal works automatically once your key and products are set — the server creates a Dodo customer-portal session for upgrades and plan management, with the return URL passed as a query parameter (a Dodo API requirement). There's nothing for you to configure there beyond the env vars on this page.
Dodo notifies the server of payment events over a webhook. You register the URL in the Dodo dashboard and copy back the signing secret so the server can verify each inbound request.
The inbound path is /webhooks/dodo (POST). Register
your public URL for it in the Dodo dashboard:
https://login.example.com/webhooks/dodo
Dodo signs webhooks per the Standard Webhooks spec
(HMAC-SHA256). The dashboard gives you a signing secret when you
create the endpoint — copy it into
QNT_DODO_WEBHOOK_SECRET. The server verifies every
inbound signature against this value and rejects anything that
doesn't match.
A mismatched or missing secret means valid webhooks fail verification, so plan changes won't be applied. Make sure the secret matches the mode (test vs live) of your API key.
Environment changes are picked up at startup, so restart the service after editing the env file, then run a smoke test.
systemctl restart qnt-server
The repo ships a smoke script at
scripts/dodo-billing-smoke.sh that exercises the
billing endpoints end to end. Run it to confirm the key,
products, and base URL line up before you flip to live mode.
scripts/dodo-billing-smoke.sh
Without QNT_DODO_API_KEY set, the billing endpoints
are inactive — billing runs in a disabled mock mode. The
dashboard still works and users can sign in; upgrades simply
don't charge anyone until you supply a key.
Billing wired up. Carry on through the admin guide.