Bybit testnet setup
Figures on this page are as of 2026-09. Fees, limits and margin tiers change — check the venue's own docs before acting on a number.
Bybit’s testnet is a separate hostname with separate credentials:
| Purpose | URL |
|---|---|
| Testnet | https://api-testnet.bybit.com |
| Mainnet | https://api.bybit.com (alternate https://api.bytick.com) |
Testnet keys do not exist on mainnet, so pointing at the wrong host produces an
authentication error rather than a live order. That is the arrangement you want,
and it is worth appreciating — OKX, by contrast, shares a hostname between demo
and production and switches with the x-simulated-trading: 1 header, where a
dropped header means production.
Mainnet also has regional domains — .nl, .tr, .kz, .ge, .ae, .eu,
.id, plus manepa.jp for Japan and spark-fintech.com for Hong Kong. Which
one applies to you affects the host, not the mechanics.
What carries over
The request mechanics are identical, which is the point of testing there:
- Headers:
X-BAPI-API-KEY,X-BAPI-TIMESTAMP(milliseconds),X-BAPI-SIGN, and optionalX-BAPI-RECV-WINDOW(default 5000 ms) - Signature: HMAC_SHA256 or RSA_SHA256
- Pre-hash string:
timestamp + api_key + recv_window + queryStringfor GET, or+ jsonBodyStringfor POST
So the signing code you get working on testnet is the signing code that runs on mainnet. Since Bybit’s pre-hash construction differs from both Binance’s and OKX’s — no method, no request path, no separators — testnet is the right place to get that right.
The difference that does not carry over
Account type. This is the Bybit-specific trap, because it changes what error codes mean rather than whether things work.
Under a Unified Trading Account, several codes differ from classic:
| Code | Classic | UTA |
|---|---|---|
10003 | “Too many sessions under the same UID” | “Your api key has expired” (spot) |
33004 | — | “Your api key has expired” (derivatives) |
10006 | — | “Too many visits. Exceeded the API Rate Limit.” |
10018 | — | “Exceeded the IP Rate Limit.” |
And two codes tell you which you are on:
110028— “The API can only be accessed by unified account users.” (you are not on UTA)100028— “The API cannot be accessed by unified account users.” (you are)
If your testnet account and your mainnet account are different types, error handling validated on one will misinterpret the other. Check that they match before you conclude your error handling works.
Migration checklist
- Base URL changed to mainnet, and logged at startup so the environment is visible rather than inferred
- Mainnet credentials in place; testnet credentials removed from the environment entirely
- Account type verified to match what you tested against — check via
110028/100028if unsure - IP binding configured if you intend to use it (
10010is its signature when it is wrong) - Key permissions confirmed —
10005is “Permission denied, please check your API key permissions” - Rate limit handling exercised, and order retries not automatic
- Region checked — HTTP
403is documented for US IPs, as well as for IP rate limit breaches and GET requests with an empty JSON body - First live orders sized small
Deriving both from one switch
The failure mode worth engineering against is not testnet credentials on mainnet — that fails immediately and loudly. It is mainnet credentials with a testnet URL still set in a code path you did not exercise.
Derive both from a single setting so they cannot disagree:
BASE, KEY, SECRET = (
("https://api-testnet.bybit.com", TESTNET_KEY, TESTNET_SECRET)
if ENV == "testnet"
else ("https://api.bybit.com", LIVE_KEY, LIVE_SECRET)
)
log.info("bybit environment=%s base=%s", ENV, BASE)
One switch, two values, one log line.
Testnet accounts are their own thing
Testnet credentials are issued from the testnet site, not from your mainnet account, and the two are unrelated. Two things follow that trip people up:
Testnet environments get reset. Balances and keys can be wiped as part of maintenance. Do not build anything that assumes a testnet key is permanent, and do not spend time debugging a testnet key that stopped working — regenerate it first and see whether that was the whole problem.
Your testnet account type is chosen independently. Which is the mechanism behind the mismatch described above: it is entirely possible to have a classic testnet account and a Unified Trading Account on mainnet without ever having made a decision about it.
What testnet cannot tell you
- Fills and slippage. Testnet liquidity is not real; execution conclusions drawn from it are not evidence.
- Rate limits in anger. Testnet traffic rarely reproduces production
pressure, and
10006versus10018— account-level versus IP-level — need different fixes. - Region restrictions. The HTTP
403for US IPs is a production concern. - Your own behaviour. A workflow that felt fine on testnet feels different when the number is real. That is not a flaw in testnet; it is outside what it simulates.
FAQ
Do Bybit testnet keys work on mainnet?
No. Testnet credentials exist only on api-testnet.bybit.com and mainnet
credentials only on api.bybit.com. This is a safety property — an environment
mix-up gives you an authentication error rather than a real trade.
Why do my error codes mean different things on mainnet?
Most likely your testnet and mainnet accounts are different types. Under a
Unified Trading Account, 10003 on spot means an expired API key, while on
classic accounts it means too many sessions under the same UID. Confirm which
type each account is — 110028 and 100028 distinguish them — before trusting
error handling you validated elsewhere.
Is the signature the same on testnet and mainnet?
Yes. Same headers, same HMAC_SHA256 or RSA_SHA256, same pre-hash string of
timestamp + api_key + recv_window + queryString or body. Testnet is the right
place to get the construction right, since Bybit’s differs from Binance’s and
OKX’s.
Can I use one key for testnet and mainnet?
No, and that is deliberate. Separate credentials per environment are what make pointing at the wrong host fail safely.