Paying for your subscribers' traffic
Partner API v1 · revision 1.13 · 9 November 2026 · What changed
The stake, the partner wallet, top-ups, what a GB costs and withdrawing the unspent deposit, in TON.
All the guides · OpenAPI spec and explorer · Need more? Talk to us · Partner terms
This page is for resellers who use the Partner API. It explains how a partner pays for its subscribers' traffic: the stake, the free trial (by agreement), the partner dashboard, linking a wallet, topping up, what a GB costs, what happens at zero balance, and how to withdraw what you did not spend.
Units. Every amount in the API is an integer number of nanoTON: 1 TON = 1,000,000,000 nano. "GB" always means GiB, 1,073,741,824 bytes, with upload and download added together.
Paths. The Partner API (server to server, with your API key) is under
https://proofkit.org/. The partner dashboard's API (signed in as the owner)
is under https://proofkit.org/. This page gives full paths.
Support. Where this page says "write to support": open a ticket under Support on proofkit.org while signed in as the partner's owner account.
See also. Partner quick start, Partner API reference, Transports and Russia, Without code: the dashboard, and the binding Partner Terms.
At a glance
| Question | Answer |
|---|---|
| How you pay | Prepaid, in TON only. The partner wallet you link deposits into the ProofKit escrow contract, and your subscribers' traffic is charged to your partner balance per GB. Only the dashboard's top-up (the contract's Deposit message) or a transfer from the linked wallet with the comment deposit is credited. A plain transfer is credited to no one. There is no credit, no invoice and no other payment method. |
| What else is charged | Nothing. There is no API fee, no monthly or set-up fee and no minimum spend. You pay only for traffic, per GB. Your wallet pays TON's own network fees. |
| Stake | A partner that signs itself up locks a stake of 5 TON with its first deposit, which is at least 5.1 TON: the stake and 0.1 TON for traffic. The stake is never charged for traffic and cannot be withdrawn while the partner runs. It comes back to the balance 30 days after you close the partner, or after ProofKit switches it off. Abuse by you or your subscribers can cost part or all of it (section 6). |
| Test access | None on sign-up: a partner that signs itself up pays for every GB from the first byte. A free trial exists only by agreement with ProofKit. It has limits that ProofKit sets, by default 7 days, 50 GB and 10 subscribers, and needs no wallet and no deposit. |
| Price per GB | The node's price plus the 20% network fee. The catalogue gives both: price_per_gb_nano (without the fee) and total_price_per_gb_nano (what you pay). olcRTC traffic has a price of its own from 20 November 2026 (see The olcRTC price). |
| Smallest top-up | 0.1 TON in the dashboard. The escrow contract itself refuses anything under 0.05 TON. |
| Minimum balance | None to keep beyond the stake. Outside a free trial, new sessions need balance_nano − stake_nano above zero (stake_nano is 0 for a partner without a stake). |
| Time to credit | Usually under a minute after the transaction confirms. |
| At zero balance | New sessions are refused. The session whose traffic took the balance to zero is disconnected at once, and every other running session you pay for within about 15 seconds. |
| Balance emails | The partner's owner gets an email when the balance has about two days of traffic left, and another when it reaches zero (section 6). Your backend is not notified: poll GET /. |
| Withdrawing | The unspent part of your deposit, to your linked wallet only. ProofKit co-signs each withdrawal and your wallet sends it. |
1. Trial, then prepaid
Every partner is on one plan at a time. GET / reports it in
billing_plan, and the dashboard shows it.
billing_plan | What it means |
|---|---|
trial | Only by agreement with ProofKit. Traffic is free while every trial limit holds. Past any limit you are on prepaid terms. |
prepaid | Every GB is charged to your balance. The balance never goes on credit. A partner that signs itself up starts here. |
exempt | ProofKit covers the traffic and nothing is charged. Only ProofKit sets this plan. |
The free trial
A free trial exists only by agreement with ProofKit. A partner that signs itself up has none:
it is on prepaid from the first GB (see Getting access). To
agree a trial, talk to us. ProofKit then
starts it and sets its three limits: an end date, a traffic allowance in GB and a subscriber
cap. The defaults are 7 days, 50 GB and 10 subscribers. A trial needs no wallet and no
deposit: a backend needs only its API key, and a partner that adds subscribers by hand only
the dashboard. ProofKit can extend a trial or end it early.
While the trial runs:
- Traffic costs you nothing. No charge is written to your balance, and
cost_nanoin usage reports is 0. - Sessions start even with an empty balance.
- Each GB your subscribers move on a priced node is counted against the allowance.
- The cap counts every subscriber you have created, disabled ones included.
DELETE /disables a subscriber but does not free its place.api/ v1/ partner/ subscribers/ {external_id} - Creating a new subscriber at the cap answers
403 TRIAL_LIMIT. This comes fromPOST /, fromapi/ v1/ partner/ subscribers POST /with an external ID you have not used before, and from the dashboard's "add a subscriber by hand". Re-provisioning a subscriber you already have, disabled or not, is never refused.api/ v1/ partner/ subscribers/ {external_id}/ olcrtc - To add subscribers past the cap, top up and ask ProofKit to end the trial early. After the trial there is no subscriber cap.
- You can top up. Nothing is taken from the balance until the trial ends.
The trial ends as soon as any limit is passed: the end date arrives or the allowance is used up. It also ends if you hold more subscribers than the cap, which can happen only if ProofKit lowers the cap. You are then on prepaid terms from the next traffic report. Nothing restarts, and no session is dropped because of the switch itself. The report that crosses the traffic allowance is the last free one, so usage can go slightly past it.
Top up before the trial ends. If your balance is zero when the trial ends, your subscribers' sessions are closed within about 15 seconds and new ones are refused, exactly as described in section 7. ProofKit sends your backend no warning when a trial is about to end or has ended.
To follow the trial, read the trial object on GET / (or on the
dashboard's GET /):
"trial": {
"until": "2026-10-03T12:00:00Z",
"max_subscribers": 10,
"subscribers": 3,
"max_bytes": 53687091200,
"bytes_used": 4563402752,
"active": true
}
active: true means traffic is free right now. active: false means a limit was passed and
you are paying. trial is absent (or null on /me) when you are not on a trial plan.
Note on billing_mode: this older field on GET / keeps its
published meaning. It reads "free" only when ProofKit covers your traffic (the exempt
plan) and "metered" otherwise, including during a trial. Use billing_plan and
trial.active to tell a free trial from paid traffic.
2. The partner dashboard
Linking a wallet and withdrawing happen only in the partner dashboard. The Partner API key cannot move money.
Who can open it. Your partner belongs to one ProofKit account, its owner: the account that signed it up in the dashboard, or the one ProofKit names for a partner it sets up by hand:
- The owner is an account that signs in with an email address. A Telegram-only account cannot be the owner, but an email account can link Telegram for two-factor codes.
- One account runs at most one partner.
- When ProofKit names the owner, that account is signed out everywhere. Sign in again.
How to get there. Sign in on proofkit.org with the owner account, then open
https://proofkit.org/. The site navigation shows a link to it for the owner
account.
Two-factor sign-in is required. Turn on an authenticator app or Telegram two-factor codes
for the owner account. Without either, every dashboard request is refused with
403 TWO_FACTOR_REQUIRED, and the page asks you to turn one on before it continues.
Every dashboard action works on your partner's billing account, never on the owner's personal ProofKit balance or wallet.
The dashboard shows:
- your plan, and the trial's use while it runs;
- the balance, and what traffic cost per day over the last 30 days;
- when your subscribers moved olcRTC traffic in those 30 days, a line under that chart: how
much it was, how much of the charges was at the olcRTC price once any traffic was charged
at it, and, while the last step of the olcRTC price schedule names a price and has not
started, what the same traffic costs at that step (left out on the
exemptplan, where nothing is charged); - the number of subscribers you have provisioned, disabled ones included;
- your stake, when you have one, and the way to close the partner (section 6);
- the partner wallet, top-up and withdrawal;
- the deposits and withdrawals made so far;
- your API key prefix and key rotation, and the per-key rate limits;
- the Subscription tab: whether subscriptions are on for you, and the subscription's branding, its route filters and the setup page's switch, which you edit there (see Subscription settings);
- when olcRTC is on for you, the olcRTC figure "in rooms / your share": how many of your
subscribers' olcRTC keys count as present now against your share. A key counts as present
while its subscriber has moved traffic through that exit in the last 5 minutes on any
line, Reality included, not only olcRTC (see olcRTC under
GET /). The share binds only once subscriptions are switched on for you; until then the figure is for information.catalogue
3. Link the partner wallet
Before the first top-up, connect the wallet that will pay and link it to the partner.
Linking uses a TonConnect ton_proof: your wallet signs a one-time challenge, which proves
that you control it. Use a wallet app that supports TonConnect.
Rules:
- Link from the dashboard on proofkit.org. The proof names the site's domain, and a challenge is valid for 15 minutes and for one attempt.
- TON mainnet only. A wallet on another network is refused (
WRONG_NETWORK). - One wallet belongs to one ProofKit account. A wallet already linked to any other
account is refused (
TON_ADDRESS_TAKEN), and that includes the owner's own personal account. A wallet that still holds another account's money in the escrow (an account that switched away from it) is refused withWALLET_HOLDS_OTHER_BALANCE: the message names that account by a masked email, and the wallet can be linked once that account has spent or withdrawn it. Use a wallet kept for the partner. - You can change the wallet at any moment. Link the new wallet in the dashboard as you linked the first one. Nothing has to be paid out, withdrawn or waited for first. What the old wallet holds of your deposit in the escrow stays on your balance and pays for your subscribers' traffic first, before the new wallet's deposit does. From then on top up from the new wallet only (a deposit from the old one is not credited), and withdrawals go to the new wallet. Only the old wallet can withdraw its own deposit: to take it out, link the old wallet again and withdraw (section 8). There is no way to unlink a wallet from the dashboard; the link only moves to another wallet.
- If the wallet has never been used on-chain and its key cannot be read, linking answers
WALLET_KEY_UNKNOWN. Send one transaction from the wallet, then connect it again.
4. Top up
A top-up is a deposit into the ProofKit escrow contract from the linked wallet. The contract keeps each deposit under the wallet that sent it, and ProofKit credits your balance because that wallet is linked to your partner.
The first deposit. A partner that signs itself up goes live with one deposit of at least 5.1 TON: a stake of 5 TON and at least 0.1 TON for traffic. Deposits add up, so two smaller ones do the same: the partner goes live once the balance reaches that sum and the Partner Terms are accepted. The smallest top-up after that is 0.1 TON in the dashboard.
Two ways to send it
- From the dashboard. Enter an amount (0.1 TON or more) and confirm in your wallet. The
wallet sends the contract's
Depositmessage. If the wallet you connect is not the linked one, the dashboard links it first, under the rules of section 3. - From any app that holds the linked wallet. Send TON to the escrow address with the
text comment
deposit: exactly that word, lower case, nothing else.deposit_linkonGET /and onapi/ v1/ partner/ account GET /is this transfer as aapi/ v1/ partner-portal/ me ton://transfer/link. It is<escrow>?text=deposit nulluntil a wallet is linked. The escrow address is also given inescrow_address.
The whole amount the escrow receives is credited. Your wallet pays its usual network fee on top. Keep about 0.1 TON in the wallet after a top-up: a later withdrawal is sent from this same wallet and needs at least 0.08 TON for its gas, and the dashboard warns you when a top-up would leave less.
What is not credited
Only two messages count: the Deposit message and a transfer with the comment deposit,
both sent from the linked wallet. Everything else is not credited:
- A plain transfer with no comment. The contract keeps it for nobody, and your balance does not change. Only ProofKit can recover it, by hand: write to support with the transaction hash.
- A transfer with any other comment. The contract rejects it.
- Less than 0.05 TON. The contract rejects it.
- Any deposit while the contract is paused. The contract rejects it. While it is paused, co-signed withdrawals are refused too (section 8).
- A transfer from any other wallet. It becomes that wallet's deposit on-chain, never the partner's. If another ProofKit account has linked that wallet (your owner's personal account, for example), it is credited to that account's balance. Otherwise it is credited to no one. This includes a withdrawal from an exchange straight to the escrow: the sender is the exchange's wallet, not yours. Move the TON to your linked wallet first, then deposit from there. TON credited to another account can reach the partner only if that account withdraws it and the linked wallet deposits it again.
When the contract rejects a transfer. A bounceable transfer comes back to the sending wallet, minus network fees. The dashboard's top-up is always sent bounceable. A non-bounceable transfer that fails stays in the contract, credited to no one: write to support with the transaction hash.
Link first, then deposit. A deposit sent before its wallet was linked is picked up once the wallet is linked, but only while it is still among the escrow's 50 most recent transactions (see below). The escrow serves the whole network, so that can pass quickly. After that, ProofKit has to credit it by hand.
How long it takes
ProofKit checks the escrow for new deposits every 30 seconds. Once your transaction is confirmed on TON, the balance normally rises within a minute. When the TON data provider limits our requests, the checks slow down, with at most about 8.5 minutes between two checks, until it recovers.
Each check reads only the escrow's 50 most recent transactions. This applies to every deposit, from a linked wallet too: if more than 50 escrow transactions pass between two checks, a deposit can be missed, and ProofKit then credits it by hand.
If a deposit sent from the linked wallet has not appeared after 15 minutes, write to support with the transaction hash.
A credited deposit shows in the balance (balance_nano on /account; on the dashboard's
/me, the balance less any stake) and in the dashboard's list of payments
(GET /).
5. What a GB costs
A route leaves through an exit node, called the origin in the API. A two-hop route enters through a second node first, the edge. The price of one GB on a route is fixed when the session starts (olcRTC traffic aside, from 20 November 2026: see The olcRTC price):
total per GB = (origin price + edge price) + 20% of that sum
On a direct route the edge price is 0. On a two-hop route, for example one that enters through an edge in Russia, you pay for both hops, and the fee is added once to their sum.
GET / lists every node you may use with:
| Field | Meaning |
|---|---|
price_per_gb_nano | The node's price for you, without the network fee. If ProofKit agreed a price for a node with you, this is that price. |
total_price_per_gb_nano | What a GB through this node costs you: price_per_gb_nano plus the fee. This is the number to plan with. |
fee_bps | The network fee in basis points: 2000 = 20%. |
entry_region | "RU" on an edge in Russia; absent elsewhere. |
A two-hop route costs the origin's total_price_per_gb_nano plus the edge's, within one
nanoTON.
Example prices (read the catalogue for real ones):
| Route | Node prices | You pay per GB | 1.5 GB costs |
|---|---|---|---|
| Direct | origin 10,000,000 | 12,000,000 (0.012 TON) | 18,000,000 |
| Two hops | origin 10,000,000, edge 10,000,000 | 24,000,000 (0.024 TON) | 36,000,000 |
Before you have a key. The Marketplace on proofkit.org (https://proofkit.org/)
shows the public nodes' prices with the 20% fee already included. That is what you pay on a
node where ProofKit has not agreed a different price with you.
Agreed prices. An agreed price is never below the node's own price, and the 20% fee is the same for every partner.
When the charge is made. Each running session reports its traffic about once a minute, and each report is charged at once: bytes × total per GB ÷ 1,073,741,824. A report under 1 MiB (1,048,576 bytes) waits and is added to the next one.
Price changes. For Reality, Hysteria2 and XHTTP traffic, a session keeps the price it started with, with one exception: if a node's own price goes down, running sessions follow it down once the lower price has stood for 3 hours, and any change of the node's price starts those 3 hours again. A session that starts during those 3 hours takes the lower price at once. If a price goes up, running sessions keep the old price, and only new sessions pay the new one. On a node where ProofKit agreed a price with you, sessions follow the agreed price instead, so a change to the node's own price does not move them. If ProofKit changes your agreed price, count on the new price only for sessions started after the change. olcRTC traffic follows its own rule, below.
Where you see the price and the cost.
POST /returnsapi/ v1/ partner/ subscribers/ {external_id}/ connect total_price_per_gb_nano, the session's price per GB with the fee, andplatform_fee_nano, the fee part of that price. A connect that returns a running session gives that session's price as it is now: the lowered total and the lowered fee once a price drop has reached it (see Price changes), the price the session started with until then.- In
GET /,api/ v1/ partner/ subscribers/ {external_id}/ sessions price_per_gb_nanois the session's price per GB with the fee. The catalogue field of the same name is the price without the fee. GET /andapi/ v1/ partner/ usage GET /give each subscriber'sapi/ v1/ partner/ subscribers/ {external_id}/ usage cost_nanofor a period, andGET /also givesapi/ v1/ partner/ usage total_cost_nano.
The olcRTC price
From 20 November 2026, olcRTC traffic has a price of its own. Until then it costs what the session costs, like the other transports. The new price is one price per GB for the whole network, whatever the exit or the route, and it is the payer's price, with the 20% network fee included. It is set and charged in TON, at the amounts below; the dollar figures are approximate, at 1 TON = $1.44. If the TON rate moves by more than 10 %, we may adjust the TON amounts; a rise is announced 30 days ahead, like any rise. A GB is 1,073,741,824 bytes, upload and download together.
| From (00:00 UTC) | olcRTC, per GB | In TON | In nanoTON |
|---|---|---|---|
| Until 20 November 2026 | The route's own price, as today | — | — |
| 20 November 2026 | About $0.03 | 0.0208 TON | 20,800,000 |
| 8 December 2026 | About $0.05 | 0.0347 TON | 34,700,000 |
| 22 December 2026 | About $0.08 | 0.0556 TON | 55,600,000 |
The rules:
- It applies when the traffic moves. The olcRTC price is read at every charge, and a session never locks it in. The other transports keep their rule: a session keeps the price it started with, or a lower one after a drop, and that does not change.
- A rise is announced at least 30 days before it starts. A cut may start at once. The
announcement is in
olcrtc_pricingonGET /and in a support ticket.account - A route through the entry in Russia adds nothing to it. olcRTC never passes the entry
node, so a GB of olcRTC costs the same on a direct route and on a
via RUroute. - The olcRTC part of a charge goes 4/5 to the exit node's operator and 1/5 to the network, as any charge does.
- The free trial covers olcRTC. While a trial is active, nothing is charged for olcRTC traffic either.
- A route priced at 0 stays free. Its olcRTC traffic is not charged either.
Your share grows with your olcRTC traffic. Your olcRTC share is the larger of two numbers: a fifth of the network's olcRTC places, rounded up, and 8 places plus one for every 15.625 GiB of olcRTC traffic your subscribers moved in the last 30 days ($1.25 of traffic at the top price of about $0.08 per GB, counted in bytes so the TON rate does not move it). Traffic inside a free trial counts. A share ProofKit agrees with you replaces this one. The share, and your part of each exit's keys, are explained in Transports and Russia.
Reading the schedule. olcrtc_pricing on GET / has two fields.
total_price_per_gb_nano is the price in force now, with the network fee, and is null while
olcRTC is still charged at each session's own price. schedule lists every step, oldest
first, each with effective_from and total_price_per_gb_nano: the steps in force, the steps
past and the steps announced. From 12 October 2026 it holds the three steps above. To
reconcile a window, cut it at the effective_from dates.
The usage reports give each subscriber olcrtc_bytes, the part of bytes that went through
olcRTC, and olcrtc_cost_nano, the part of cost_nano charged at the olcRTC price. They also
give olcrtc_list_cost_nano: the same bytes at the price of the last step of the schedule (the
step of 22 December 2026 while it holds the three steps above), whether or not its date has
come. That is a quote, not a charge. The dashboard's olcRTC line, under the charges chart,
reads the same way: your subscribers' olcRTC traffic of the last 30 days, what was charged at
the olcRTC price and, until the last step starts, what that traffic costs at it. See
GET /.
6. The balance and the charges
Your balance is: deposits − traffic charges − withdrawals − slashes. A slash is a cut that
ProofKit makes in a partner's stake; a partner without a stake has none (see stake_nano in
GET /).
- The dashboard shows the balance less any stake, which is the figure the connect check works
from, and what your subscribers' traffic cost per UTC day over the last 30 days
(
charges_30donGET /: one entry per day with a charge, oldest first).api/ v1/ partner-portal/ me - Operators are paid from your escrow deposit afterwards. An operator's claim takes the charge from your deposit on-chain, and the contract splits it 80% to the operator and 20% to ProofKit. The claim does not change your balance, because the charge was already booked when the traffic moved. For a while, the escrow therefore holds more than your balance. The withdrawal limit (section 8) accounts for the difference.
The stake. A partner that signs itself up has a stake of 5 TON locked from the moment it
goes live; a partner ProofKit sets up by hand has none unless one is agreed. The stake stays in
your escrow deposit and in balance_nano, but it is never charged for traffic and cannot be
withdrawn while the partner runs: the connect check and the withdrawal limit both leave it out.
It comes back to the balance 30 days after you close the partner in the dashboard (Close the
partner…) or ProofKit switches it off. Until then you can reopen a partner you closed
yourself, and it is live again at once with the stake still locked. Abuse by you or your
subscribers can cost part or all of it: ProofKit cuts a stake by hand, with a reason, emails
the owner the reason, and you can appeal in a support ticket. A cut lowers balance_nano and
stake_nano by the same amount, so balance_nano − stake_nano does not move, and the partner
goes on running.
Quotas do not cap the bill. A subscriber's quota_bytes is a control the platform
enforces (sessions end when the allowance is used), not a boundary on what is charged:
every byte that moved is billed as it always was, including the overshoot that lands
while a cut is on its way. The quota's used bytes and your invoice read the same metered
intervals, so they can never disagree about the same traffic.
Watching the balance. ProofKit emails the partner's owner when the balance has about two
days of traffic left, and again when it reaches zero (the emails are described below). It does not
notify your backend. Poll GET /: the connect check works from
balance_nano − stake_nano, less any stake locked in nodes your billing account owns (none
unless it runs nodes). stake_nano is 0 for a partner without a stake, so for most partners
that is balance_nano. That response also has credit_limit_nano. The check is
balance_nano − stake_nano > -credit_limit_nano, and on the prepaid plan credit_limit_nano is
always 0, so a new session needs balance_nano − stake_nano above zero. The dashboard shows a
warning when you pay for traffic and the balance is under 1 TON.
The balance emails. Two emails go to the partner's owner:
- About two days left. At the pace of the last seven days' charges, the balance covers less than two days.
- At zero. The balance has reached zero (section 7): new sessions are refused and running ones close.
Neither is sent for a partner that was charged nothing in the last seven days. Each email is
sent at most once a day, with one exception: after a top-up has lifted the balance above zero,
reaching zero again is emailed again at once. They go to the owner account's email address,
once that address is confirmed, and each also appears in the owner's bell on proofkit.org. A
partner inside a free trial, on the exempt plan, or switched off gets neither.
7. When the balance reaches zero
"Zero" means a balance (balance_nano − stake_nano, section 6) of 0 or less. On the prepaid plan
there is no credit.
- New sessions are refused.
POST /andapi/ v1/ partner/ subscribers/ {external_id}/ connect POST /answerapi/ v1/ partner/ subscribers/ {external_id}/ olcrtc 400 INSUFFICIENT_BALANCEon a priced route (a route priced at 0 still connects). A call for a route the subscriber still holds returns that session until the enforcer closes it (step 3). A subscription fetch, throughGET /or the hosted URL, starts no new routes. The routes it would have started are left out of the answer.api/ v1/ partner/ subscribers/ {external_id}/ subscription - The session whose report took the balance to zero is ended on that report, and its credentials are removed from the node at once, so its line stops working.
- Every other active session you pay for on a priced route is closed within about 15 seconds. A session still pending (its node has not confirmed it) is closed once it turns active; one on a route priced at 0 keeps running. The session enforcer runs every 15 seconds. It checks your balance once more just before it acts, so a top-up that arrived in the meantime keeps the sessions. For each session it closes, it removes the subscriber's credentials from the node and then sends the node its full list of live sessions. This covers sessions started through the API and through a subscription alike. Your subscribers' olcRTC keys are withdrawn at the next room check, which runs about every 5 minutes.
- The balance can end slightly below zero. Charges are booked as reports arrive, so the traffic reported in those last seconds is still charged. Your next deposit covers it first.
After a top-up, nothing reconnects by itself. Any positive balance lets sessions start again:
- With the API, connect the subscriber again.
- With a subscription, through the API or the hosted URL, the next fetch starts the missing routes. Clients fetch again at the subscription's update interval (1 hour by default), and a subscriber can also refresh by hand.
- A new session gets new credentials, so the lines a subscriber had before stop working, and a refreshed subscription carries the new ones.
Charges arrive every minute from every running session. A small balance lasts only a short time, so keep enough to cover your peak traffic between two top-ups.
If ProofKit suspends your partner
A suspension switches your partner off and ends your subscribers' sessions:
- The Partner API key answers
401 INVALID_PARTNER_KEY, and the dashboard answers404 NOT_A_PARTNEReverywhere but its Balance tab. - Every running session of your subscribers is ended at once, so nothing is charged to your billing account after that. Switching the partner back on does not bring the sessions back by itself.
- Your subscribers' hosted subscription URLs fall back to our unbranded standard lists.
- Your deposit stays yours: the Balance tab stays open, and you withdraw what is left there. A stake you hold stays locked for 30 days after the suspension, and then comes back to the balance.
8. Withdraw the unspent deposit
You can take back the part of your deposit that your subscribers' traffic has not spent.
Which wallet. Only the linked wallet. ProofKit signs each withdrawal for that wallet's address. The contract accepts the signature only from that wallet, takes the amount from the deposit held under it and pays it back to that same wallet. A withdrawal cannot be sent to any other address. After you change the linked wallet, what the previous wallet holds can be withdrawn only from that wallet: link it again (section 3), then withdraw.
Steps.
- In the dashboard, enter an amount. The dashboard shows what is in the escrow, what is pending and what can be withdrawn now.
- ProofKit checks the limit and signs an approval, which is valid for 5 minutes
(
POST /).api/ v1/ partner-portal/ wallet/ withdraw-approve - Your wallet sends the signed
Withdrawmessage to the escrow. It attaches 0.05 TON for network fees, and the contract returns the unused part to the wallet as a separate "ProofKit gas refund" message. Keep at least 0.08 TON in the wallet for this; the dashboard checks before it asks for an approval. - The contract pays the amount to your wallet with the comment "ProofKit withdrawal". ProofKit sees it at its next check, usually within a minute, and deducts it from your balance.
The limit. GET / returns
max_withdrawable_nano:
max_withdrawable = min( balance − stake,
deposit held in the escrow under your linked wallet − pending )
pending = traffic charges not yet paid out to operators
+ operator claims signed but not yet on-chain
+ withdrawal approvals issued in the last 10 minutes
(the last hour when the escrow's figure cannot be read)
The response also gives on_chain_deposit_nano (the escrow's own figure for your linked
wallet, or null if it could not be read; ProofKit then estimates it from its records),
pending_charges_nano, vpn_balance_nano and estimated_on_chain_nano,
max_per_withdrawal_nano with max_per_withdrawal_set_by (the most one withdrawal can ask
for: see Other limits below), and
previous_wallets: the wallets you deposited from before you linked the current one, each as
{"address": "0:…", "held_nano": …} (the address in raw form, and your part of that wallet
from ProofKit's records, no escrow read), oldest money first. It is an empty list for nearly
every partner. Normally the limit equals your balance less any stake (stake_nano, 0 for a
partner without one). It is lower while a claim or an earlier withdrawal is in flight, and
while a previous wallet holds part of your deposit: the limit counts the linked wallet only.
Other limits.
The dashboard's smallest withdrawal is 0.05 TON.
ProofKit's signer limits what it signs in three ways:
- one withdrawal is signed for at most a set amount;
- the signer's signatures of a UTC day add up to at most a set total, shared by every withdrawal and operator payout on the network, and the day starts again at 00:00 UTC;
- more than 10 signing requests arrive within a minute (withdrawals and operator payouts count together). Try again a minute later.
The two amounts are ProofKit's to set and can change, so the limit states what applies now.
max_per_withdrawal_nanois the most one withdrawal can ask for: the smaller ofmax_withdrawable_nano, the amount one withdrawal is signed for, and what the day's total still leaves.max_per_withdrawal_set_bysays which of the three sets it:withdrawable,per_transactionordaily. Both arenullwhile the signer's limits cannot be read, and thenmax_withdrawable_nanois all there is to go by. The dashboard says it in one line under the withdrawable amount and fills MAX with it.An amount above it is refused, and nothing is held: no approval is recorded, or the one recorded is removed, so the amount does not count as pending.
400 WITHDRAW_ABOVE_SIGNING_LIMITsays "A withdrawal is signed for at most N TON at a time: ask for N TON or less."400 WITHDRAW_SIGNING_DAILY_LIMITsays "Withdrawals have reached today's signing limit: ask for a smaller amount, or try again after 00:00 UTC." When the signer answers that it cannot sign for another reason (the rate limit above), or cannot be reached, the answer is500 INTERNAL_ERRORwith "The withdrawal could not be signed just now. Try again in a few minutes.", and nothing is held either. When the signer does not answer in time, or its answer is lost, it is not known whether it signed: the answer is500 INTERNAL_ERRORwith "The withdrawal could not be confirmed just now. Its amount stays held for about 10 minutes, or up to 60 minutes if the escrow cannot be read, and is free again after that if no transaction reaches your wallet. Try again then." and the amount counts as pending for that long, as an approval does. Before you withdraw several hundred TON in one day, write to support.An approval that your wallet does not use within its 5 minutes expires, and its amount counts as pending for 10 minutes after it was issued (twice its lifetime, a margin for clock differences). If you cancel the prompt in your wallet, the amount is back in the limit 10 minutes after you asked for the approval. While the escrow's figure cannot be read (
on_chain_deposit_nanoisnull), an approval counts as pending for an hour instead: until ProofKit's payment checker books a withdrawal, its records cannot tell one that went through from one that did not.Withdrawing everything is the same as running out. Once the withdrawal is booked, your balance is zero and your subscribers' sessions are closed (section 7).
If ProofKit cannot co-sign. While the escrow contract is paused, it refuses deposits and
co-signed withdrawals. Once it has been paused for a net 30 days, every depositor can take
its whole deposit out without ProofKit's signature. The wallet that holds the deposit sends
the contract's SelfWithdraw message: a message body of only the 32-bit op code
0xf1a2b3c4. The contract must still be paused when it arrives. Attach about 0.05 TON for
gas, as for a withdrawal; the unused part comes back. The dashboard has no button for this.
"Net" means that paused time counts up and time running unpaused counts it back down,
second for second. So unpausing and pausing again does not restart the count, but the
unpaused time in between is taken off it.
9. Reference
Dashboard API
These routes serve the dashboard. They need the owner's signed-in session (a JWT access
token in Authorization: Bearer …), with two-factor sign-in turned on. They do not accept
the Partner API key. Base: https://proofkit.org/.
| Method and path | What it does |
|---|---|
GET / | partner_id, name, the plan (billing_mode: exempt / trial / prepaid, as stored), trial, balance_nano (the balance less any stake), wallet, escrow_address, deposit_link, subscribers (disabled and removed ones included), subscribers_removed (how many of them you removed), api_key_prefix, previous_key_expires_at (while a rotated-out key still works, else null), rate_limit_per_min (Partner API requests per minute per key), subscription_rate_limit_per_min (subscription fetches per minute per key), olcrtc ({in_use, share}: your subscribers' olcRTC keys that count as present now, and your share, which binds only once subscriptions are on; null while olcRTC is off for you), charges_30d, olcrtc_30d (your subscribers' olcRTC traffic over the same 30 days: bytes; cost_nano, what it was charged at the olcRTC price, 0 while none was in force and for traffic that was not charged (a trial, the exempt plan); list_effective_from and list_total_price_per_gb_nano, the last step of the olcRTC price schedule, when it names a price (a date, and a price per GB with the network fee); list_cost_nano, bytes at that price, what the same traffic costs once the step is in force (a list quote, not a charge: nothing is charged on the exempt plan or inside a trial); the three list_ fields are null when the schedule is empty or its last step names no price) |
GET / | {payload, expires_in: 900}: a one-time challenge for the wallet's ton_proof, valid 15 minutes |
POST / | {address, network, state_init, proof}, from TonConnect's account.address, account.chain, account.walletStateInit and tonProof.proof → {"linked": true} |
GET / | Deposits and withdrawals, newest first, at most the latest 100 |
GET / | The withdrawal limit and its parts (section 8) |
POST / | {amount_nano, user_ton_address} → {amount_nano, nonce, valid_until, sig_r, sig_s}, valid 5 minutes. user_ton_address is the linked wallet in raw form: 0: followed by 64 hex digits. A friendly EQ… or UQ… address is refused (INVALID_ADDRESS). An amount above the signer's limits is refused with WITHDRAW_ABOVE_SIGNING_LIMIT or WITHDRAW_SIGNING_DAILY_LIMIT and holds nothing (section 8). |
POST / | A new Partner API key, shown once: {api_key, api_key_prefix, previous_key_expires_at}. The key it replaces keeps working for 24 hours. A key rotated out earlier than that stops at once. At most 5 rotations an hour. |
GET / | The subscription's settings: the route filters and branding you set, whether a subscriber's link shows a setup page in a browser, what an app that names no device gets from a subscriber with a device limit (sub_device_mode: strict or soft), whether subscriptions and olcRTC are on, your olcRTC share, and a preview of the first line names. See Subscription settings. |
PUT / | Changes any of the filters, the branding fields, the setup page's switch and the device mode the body names (left out = unchanged, null clears a field that can be empty; a sub_device_mode other than strict or soft is 400 INVALID_DEVICE_MODE) and answers as GET /. At most 30 writes an hour. |
GET / | The partner's subscribers a page at a time, newest first: limit (1 to 200, 50 when left out), cursor (the last answer's next_cursor), q (matches anywhere in the external_id, whatever the case), state (active, expired, used_up or disabled) and order (newest or oldest) → {subscribers, next_cursor, counts: {all, active, expired, used_up, disabled, removed}}. The counts are of all the subscribers the list holds, whatever q says, and removed of the ones you removed, which it does not hold. These parameters are the dashboard's own: the Partner API's GET / keeps its four. A subscriber carries device_limit (null for none) and devices, the devices that hold a place now. |
POST / | {external_id, quota_bytes?, exit_countries?, expires_at?, device_limit?, only_new?} → the subscriber as the dashboard lists it (traffic, last seen, its limits, happ_url, ghostlane_url) and created: 201 when new, 200 when it existed: it is enabled again if it was disabled, and the limits you name are written on it — a quota_bytes among them starts its allowance again from now — while a limit you leave out stays as it was. POST /' rules: INVALID_EXTERNAL_ID, TRIAL_LIMIT, and for the four limits INVALID_QUOTA, INVALID_EXIT_COUNTRIES, INVALID_EXPIRES_AT, INVALID_DEVICE_LIMIT; 409 SUBSCRIPTION_NOT_ENABLED until ProofKit switches your subscriptions on. With only_new: true, a number that exists is answered 200 with left_as_is: true and nothing is done to it: the dashboard asks before it enables one, because its old link would work again. A number you removed comes back fresh, with or without only_new: 200 with restored: true and created: false, enabled, with the link its removal made and only the limits this call names. Adds are counted in subscribers, at most 300 an hour, this call one. |
POST / | {external_ids: [1 to 100], quota_bytes?, exit_countries?, expires_at?, device_limit?} → {results: [{external_id, outcome, code?, message?}]}, a line a number, in order: created (with the limits), existed (left exactly as it is: not enabled, not given the limits), restored (one you removed, back fresh with the limits; it takes no trial place, so it is brought back after a trial's stop too) or refused. Everything is checked before anybody is made: INVALID_BATCH for none or more than 100; INVALID_EXTERNAL_ID and DUPLICATE_EXTERNAL_ID name the line (a number is 1 to 128 bytes and holds no tab or other control character); the limits refuse as above; 409 SUBSCRIPTION_NOT_ENABLED as the single add. A trial at its cap ends the batch there: the numbers after it that would be made are refused with TRIAL_LIMIT, and the ones that exist are existed. A fault of ours ends it with NOT_ADDED. The batch runs to its end whatever becomes of the request. It spends one of the hour's adds for each number it names, and a batch that does not fit is refused whole (429). No link is in the answer. |
GET / | One subscriber's card: the row as the list gives it, with days — [{day, bytes, cost_nano}], its metered traffic on the 30 UTC days ending today, oldest first, a day without traffic left out — and routes — [{exit_country, entry_country, bytes, started_at, last_traffic_at}], its live sessions, the one that moved last first (bytes since the session began, bytes_24h in the last 24 hours; entry_country null = a direct route) — and today, the UTC day the 30 end on, and device_list: its devices as GET / gives them (the row's devices counts those that hold a place). A read: it changes nothing. 404 NOT_FOUND for a number you do not have. |
POST / | {quota_bytes?, exit_countries?, expires_at?, device_limit?}, each left out = unchanged, null = cleared: what PATCH / does. It answers the subscriber as the dashboard lists it. A device_limit outside 1 to 100 is 400 INVALID_DEVICE_LIMIT, and one bad field writes nothing. At most 100 an hour. |
GET / | The devices that fetched the subscriber's link, the one seen last first: [{device_id, os, os_version, model, app, first_seen_at, last_seen_at, holds_slot, removed_at}], as the Partner API's GET …/ gives them. device_id is the hash we keep, never the app's own id; holds_slot says whether the device counts toward the limit now. The card carries the same list as device_list. A read: it changes nothing and spends no budget. A disabled subscriber has its devices here, where the Partner API answers 404 for it. 404 NOT_FOUND for a number you do not have. |
POST / | Removes a device: it is refused from its next fetch, with a limit or without, until you let it back, and an app that refreshes drops its servers. Nothing is ended on a node. 204, and the same call again is 204. 404 DEVICE_NOT_FOUND for a device_id the subscriber does not have. It spends one of the 100 limit changes an hour that the limits editor uses. |
POST / | Lets a removed device back. 204. While every place is held, a device seen within the last 30 days is refused with 409 DEVICE_LIMIT and nothing is written: remove another device or raise the limit first. One silent for longer comes back without a place and is judged like a new device at its next fetch. A device that is not removed is let back already. 404 DEVICE_NOT_FOUND for a device_id the subscriber does not have. It spends one of the 100 limit changes an hour that the limits editor uses. |
POST / | The subscriber leaves the dashboard for good: its connections end, its olcRTC keys are revoked, and its link is replaced by one that is switched off. 204. The account stays: its traffic stays in your totals and in GET /, and in a trial it keeps its place. The dashboard lists it no more and answers 404 for it, and so does this call a second time. To the Partner API it is a disabled subscriber. Adding the number again from the dashboard brings it back fresh. At most 100 an hour. |
POST / | {external_id}: another number for the subscriber, by the add's rules (INVALID_EXTERNAL_ID). Its link, limits and history stay. A number another subscriber holds is 409 NUMBER_TAKEN, and one held by a subscriber you removed is 409 NUMBER_REMOVED. The same number again changes nothing. It answers the subscriber as the dashboard lists it, under its new number. It shares the 30 writes an hour of the subscription settings. |
GET / | {countries: [{code, direct_from_nano, via_entry_from_nano}], partner_default}: the exit countries a subscriber's own list can name — every country its routes can exit in by your entry setting, whatever your own exit list says — each with the cheapest price per GB of a direct route and of a route through an entry (with the network fee, at your price; null where your entry setting keeps none). partner_default is your own exit list, null for every exit. |
On /me, billing_mode is the stored plan. On the S2S GET /, the
plan is in billing_plan, and billing_mode means "free" or "metered" (section 1).
After a trial passes one of its limits, both still say trial, with trial.active: false,
until ProofKit moves the partner to prepaid. Either way you are paying.
Errors you may meet
Errors come back as {"error": {"code": "…", "message": "…"}}.
| Code | Status | Where | Meaning |
|---|---|---|---|
UNAUTHORIZED | 401 | dashboard | Not signed in, or the session has expired. Sign in again. |
NOT_A_PARTNER | 404 | dashboard | This account does not run a partner, or the partner is suspended (see If ProofKit suspends your partner). A suspension does not take your deposit: the Balance tab stays open to withdraw it. |
TWO_FACTOR_REQUIRED | 403 | dashboard | Turn on an authenticator app or Telegram two-factor codes. |
BILLING_NOT_CONFIGURED | 409 | dashboard | ProofKit has not finished setting up the partner's billing. |
RATE_LIMITED | 429 | key rotation, subscription settings, dashboard subscriber writes | In an hour, refused ones included, more than any of these: 5 key rotations; 30 writes shared by the subscription settings, Reset, Disable, Enable and Change number; 300 subscribers added from the dashboard; 100 limit changes, which a device's Remove and Allow again are counted in; 100 removals. |
NUMBER_TAKEN | 409 | Change number | Another subscriber has that number. |
NUMBER_REMOVED | 409 | Change number | A subscriber you removed holds that number: add the number again to bring it back, or choose another. |
DEVICE_NOT_FOUND | 404 | device Remove, Allow again | The subscriber has no device by that device_id. |
DEVICE_LIMIT | 409 | device Allow again | Letting the device back would make it hold a place past the subscriber's limit: remove another device or raise the limit. |
INVALID_FILTER | 400 | subscription settings | An exit country, entry or transport value outside the rules, or a country or transport listed twice; the message names the field. |
INVALID_BRANDING | 400 | subscription settings | A title, URL, prefix or refresh interval outside the limits; the message names the field. |
INVALID_DEVICE_MODE | 400 | subscription settings | A sub_device_mode other than strict or soft; nothing was saved. |
FIELD_NOT_EDITABLE | 400 | subscription settings | The body named sub_enabled, olcrtc_enabled or olcrtc_max_concurrent, which only ProofKit sets. Nothing was saved. |
INVALID_ADDRESS | 400 | wallet link, withdrawal | The address is not a raw TON address (0: followed by 64 hex digits). |
WRONG_NETWORK | 400 | wallet link | The wallet is not on TON mainnet. |
PROOF_INVALID | 400 | wallet link | The challenge expired or was already used, or the proof did not check out. Connect again. |
TON_ADDRESS_TAKEN | 400 | wallet link | The wallet is linked to another ProofKit account. |
WALLET_HOLDS_OTHER_BALANCE | 400 | wallet link | The escrow still holds another account's money under the wallet (an account that switched away from it). The message names that account by a masked email; the wallet can be linked once that account has spent or withdrawn it (section 3). |
WALLET_KEY_UNKNOWN | 400 | wallet link | Send one transaction from the wallet, then connect it again. |
INVALID_AMOUNT | 400 | withdrawal | The amount is 0 or less. |
WALLET_NOT_LINKED | 400 | withdrawal | The address is not the linked wallet. |
INSUFFICIENT_BALANCE | 400 | withdrawal, connect, olcRTC | More than the balance was asked for, or the balance is zero. |
EXCEEDS_WITHDRAWABLE | 400 | withdrawal | Above max_withdrawable_nano. |
WITHDRAW_ABOVE_SIGNING_LIMIT | 400 | withdrawal | More than the signer signs for in one withdrawal; the message names the most. Nothing is held (section 8). |
WITHDRAW_SIGNING_DAILY_LIMIT | 400 | withdrawal | The signer's total for the UTC day leaves no room for the amount: ask for less, or try again after 00:00 UTC. Nothing is held (section 8). |
INTERNAL_ERROR | 500 | withdrawal | The signer answered that it could not sign for another reason (the rate limit), or could not be reached; try again in a few minutes. Nothing is held (section 8). |
INTERNAL_ERROR | 500 | withdrawal | The message starts "The withdrawal could not be confirmed": the signer did not answer in time, so it may have signed. The amount counts as pending for about 10 minutes (up to an hour while the escrow's figure cannot be read) and is free again after that (section 8). |
TRIAL_LIMIT | 403 | POST /, POST /, dashboard add | The trial's subscriber cap is reached. Top up and ask ProofKit to end the trial. |
SUBSCRIPTION_NOT_ENABLED | 409 | dashboard add | Subscriptions are not switched on for your partner, so a subscriber added by hand would get no routes. Write to support: ProofKit switches them on. The dashboard turns the form off and says so while they are off. |