API reference
The public endpoints used by the BIM VPN apps and the checkout page. Everything here is taken from the server code. Administrative endpoints are not part of the public API and are not documented.
| Service | Base URL |
|---|---|
| Accounts and payments | https://api.bimvpn.xyz |
| Exit network | https://exit-api.bimvpn.xyz |
Requests and responses are JSON. Errors come back as {"detail": ...}, where detail is either a string or an object with an error field.
Authentication
Sign-in returns two tokens:
- Access token: send it as
Authorization: Bearer <access_token>. It is short lived (expires_inseconds, one hour by default). - Refresh token: exchange it for a new pair with
/auth/refresh. It is bound to themachine_idit was issued for and is rotated on every use.
machine_id is a stable identifier for the device, chosen by the client.
POST /auth/register
Create an account. Give an email, a username, or both.
{"email": "you@example.com", "username": "", "password": "...", "machine_id": "..."}
Response 200:
{
"access_token": "...", "refresh_token": "...", "expires_in": 3600, "ip_token": "...",
"user": {"id": "9f2c...", "email": "you@example.com", "plan": "free"}
}
| Status | detail |
|---|---|
| 409 | Email already registered or Username already taken |
When an email is given, an activation link valid for 24 hours is emailed.
POST /auth/login
{"identifier": "you@example.com or username", "password": "...", "machine_id": "..."}
Response 200: same shape as register, with the account's current plan.
| Status | detail |
|---|---|
| 401 | Invalid credentials |
POST /auth/refresh
Header X-Machine-Id: <machine_id>. Body:
{"refresh_token": "..."}
Response 200: {"access_token", "refresh_token", "ip_token", "expires_in"}. The refresh token you sent is revoked; store the new one.
| Status | detail.error |
Meaning |
|---|---|---|
| 401 | invalid_refresh |
Token malformed or not a refresh token |
| 401 | reuse_detected |
Token was already used; every session of the account is signed out |
| 401 | device_mismatch |
X-Machine-Id differs from the device the token was issued to |
POST /auth/logout
Bearer auth. Body {"refresh_token": "..."}. Revokes that session. Returns 204.
POST /auth/logout-all
Bearer auth. Signs out every session of the account on every device, immediately. Returns 204.
GET /auth/sessions
Bearer auth. Lists active sessions: jti_short, device_id, created_at, last_used_at, ip, ua, app_version, is_current. Pass ?current=<jti_short> to flag the calling session.
DELETE /auth/sessions/{jti_short}
Bearer auth. Revokes one session. Returns 204 whether or not it matched.
GET /me
Bearer auth. Response:
{
"id": "9f2c...", "email": "you@example.com", "username": "", "plan": "gold",
"device_count": 1, "max_devices": 3, "sub_expires": "2026-10-15",
"email_verified": true, "ip_token": "...", "update_nudge": null
}
POST /auth/resend-verification
Bearer auth. Sends a new activation link. Returns {"ok": true}. 400 with Email already verified if there is nothing to do.
GET /auth/verify-email?token=...
The link in the activation email. Returns an HTML page, not JSON.
Payments
GET /payment/coins
Public, no authentication. The coins the server accepts right now, so a client never hard-codes availability.
{
"coins": [
{"id": "btc", "label": "Bitcoin"},
{"id": "xmr", "label": "Monero"},
{"id": "usdt-erc20", "label": "USDT (Ethereum)"},
{"id": "usdt-trc20", "label": "USDT (Tron)"},
{"id": "bim-base", "label": "BIM (Base)", "symbol": "BIM", "chain_id": 8453,
"token": "0x555fff48549c1a25a723bd8e7ed10870d82e8379",
"pricing": "dex_twap", "twap_secs": 1800, "invoice_ttl_secs": 1800}
]
}
| Field | Meaning |
|---|---|
id |
Value to send as coin to POST /payment/invoice |
label |
Name to show at checkout |
symbol, chain_id, token |
Token coins only: ticker, EVM chain id and token contract |
pricing, twap_secs |
Token coins priced from the market: dex_twap is a time-weighted average over twap_secs seconds |
invoice_ttl_secs |
How long the invoice amount is held |
A coin that is temporarily unavailable, for example BIM checkout paused because of abnormal market conditions, may be refused when you create an invoice.
POST /buy-session
Bearer auth. Returns a one-hour token that lets the checkout page act for the account without a password:
{"s": "...", "expires_at": 1789370000}
POST /payment/invoice
Auth: Authorization: Bearer <access_token>, or ?s=<buy session>.
{"tier": "gold", "period": "monthly", "coin": "usdt-trc20"}
| Field | Values |
|---|---|
tier |
bronze, gold |
period |
weekly, monthly, quarterly, yearly |
coin |
btc, xmr, usdt-erc20, usdt-trc20, bim-base. Use GET /payment/coins for the coins enabled right now. |
Response 200:
{
"invoice_id": "a1b2c3d4e5f6", "coin": "usdt-trc20", "address": "T...",
"amount": "9.990417", "amount_discriminator": 417,
"expires_at": 1789368000, "qr_data": "T...?amount=9.990417",
"status_url": "/payment/invoice/a1b2c3d4e5f6"
}
- The invoice expires 30 minutes after creation.
amountmust be paid exactly. Its last four decimals are unique among pending invoices for that coin.- Asking again with the same tier, period and coin returns the same pending invoice. Changing any of them cancels the old one.
| Status | detail |
|---|---|
| 400 | {"error": "invalid_tier"}, invalid_period, invalid_coin |
| 401 | invalid_token |
| 502 | {"error": "btc_rate_unavailable"} or xmr_rate_unavailable |
| 503 | {"error": "usdt_not_configured"}, btc_not_configured, xmr_not_configured |
GET /payment/invoice/{invoice_id}
Same auth as above. Poll this until status changes.
{
"invoice_id": "a1b2c3d4e5f6", "status": "pending", "coin": "usdt-trc20",
"amount": "9.990417", "address": "T...", "expires_at": 1789368000,
"paid_amount": null, "paid_at": null, "tx_hash": null
}
status |
Meaning |
|---|---|
pending |
Waiting for the payment |
paid |
Payment matched, plan credited |
expired |
30 minutes passed without a match |
cancelled |
Replaced by a newer invoice |
404 {"error": "not_found"} for an unknown invoice or one that belongs to another account.
Service
GET /health
Liveness check.
GET /version?platform=
Latest app build for a platform, for example macos-mojave-compat or windows-x64:
{"latest": "1.2.44", "min": "0.2.32", "url": "https://bimvpn.xyz/downloads/...", "sha256": "..."}
Exit network
Base URL https://exit-api.bimvpn.xyz.
These endpoints are served by exit broker 1.1.0.
Key auth: Authorization: Bearer bxk_<key id>.<secret>. Keys are issued by BIM. Errors use the shape {"detail": "<code>"}.
GET /healthz
{"ok": true, "connected_exits": 1, "time": 1789362814}
Broker 1.1.0 also returns "version": "1.1.0".
GET /v1/policy
The signed policy bundle every exit node enforces, published so anyone can check what an exit is allowed to carry. Its body includes version, expires_at and allowed_ports (currently [443]).
GET /v1/exits
Key auth. The active nodes this key may use: public nodes and nodes owned by the key's owner. Private nodes of other owners never appear.
{"owner": "9f2c...",
"exits": [{"node_id": "nd_EXAMPLE", "label": "Paris home", "geo": "FR",
"visibility": "private", "mine": true, "online": true,
"proxy_username": "bxk_EXAMPLE-node-nd_EXAMPLE"}]}
owner is the BIM VPN account id (user.id from /auth/login). geo is an ISO 3166 two-letter code, ZZ when unknown.
| Status | detail |
|---|---|
| 401 | unauthorized |
GET /v1/nodes/mine
Key auth. Every node owned by the key's owner, in any state, with the reason a node is paused.
{"owner": "9f2c...",
"nodes": [{"node_id": "nd_EXAMPLE", "state": "quarantine",
"state_reason": "new node awaiting activation",
"visibility": "private", "label": "", "geo": "ZZ", "online": false}]}
state is one of quarantine, active, drain, stop.
PATCH /v1/nodes/{node_id}
Key auth, for a key whose owner owns the node. Body, all fields optional:
{"visibility": "public", "label": "Paris home"}
| Field | Values |
|---|---|
visibility |
public or private |
label |
Up to 64 characters |
Response: the node object as in /v1/nodes/mine, plus owner.
| Status | detail |
Meaning |
|---|---|---|
| 401 | unauthorized |
Missing, wrong or revoked key |
| 404 | unknown_node |
No such node, or the key does not own it |
| 403 | admin_only_field |
The body tried to change owner or geo |
| 422 | bad_visibility, bad_label |
Invalid value |
Customer proxy
exit-api.bimvpn.xyz:3128, HTTP CONNECT with Basic auth. Routing hints go in the proxy username. See Connect through an exit node for the username forms, usage and error codes.
Plain HTTP
The proxy hop on port 3128 is not encrypted, so credentials sent to it are readable on the network path.
