mrxsim
Official Python client for MRXSIM.COM — purchase virtual numbers and retrieve SMS OTPs for every catalog service (Telegram, WhatsApp, Google, Instagram, Discord, Snapchat, Other (SMS), and more).
· Current version: 1.0.9 · Python 3.10+ · MIT licence · Developer Docs
This repository contains the public client library and examples only. It talks to the documented MRXSIM HTTP API; it contains no server code, no provider integrations and no pricing or routing logic.
Security first
- No hardcoded API keys in library source or examples. Every example reads
MRXSIM_API_KEY. - Prefer the environment variable
MRXSIM_API_KEY, or a localconfig.jsonthat is gitignored. - Never commit a live key. If one is exposed, revoke it immediately (Profile → API key) and create a new one.
- Examples that spend balance (
buy_number_example.py,idempotent_purchase_example.py,direct_api_example.py) do nothing but print a dry run unless you pass--confirm-purchase.
Acceptable use
MRXSIM is built for developer testing/QA, automation and integration testing against accounts you are authorized to operate, and verification for accounts you own or manage. It is not for bypassing a third-party platform's anti-abuse controls, ban evasion, fake-account creation, KYC evasion, verifying accounts you do not own or administer, or financial-account verification. See the Developer Docs for the full policy.
Install
pip install mrxsim
From this repository (editable, with the test tools):
pip install -e ".[dev]"
Configure your key
- Open https://mrxsim.com and create an account, then top up your wallet.
- Profile → Get API KEY (shown once). Keys start with
mrxs_. - Provide it to your program in one of three ways:
# a) environment variable (Linux / macOS)
export MRXSIM_API_KEY="mrxs_your_key_here"
# Windows (new shell afterwards): setx MRXSIM_API_KEY "mrxs_your_key_here"
# b) a .env file: copy the template, edit it, load it into your shell (the SDK reads real environment variables)
cp .env.example .env
set -a; source .env; set +a
# c) a gitignored config file
cp config.example.json config.json # then edit api_key, country, service
MRXSIM_API_KEY overrides api_key in config.json when both are set. MRXSIM_BASE_URL overrides the API origin (default https://mrxsim.com).
Quick start
from mrxsim import Client
with Client() as client: # reads MRXSIM_API_KEY
print(client.balance()) # {"balance_usdt": "12.34"}
# Discover the catalog: no purchase, no charge.
countries = client.list_countries()
services = client.list_services("england")
options = client.list_options(country="england", service="telegram")
order = client.get_number(
country="england", service="telegram", operator="any",
idempotency_key="my-own-stable-id-for-this-purchase", # optional but recommended, see "Retry safety"
)
print(order["phone_number"], order["id"])
sms = client.wait_for_sms(order["id"]) # polls until the OTP arrives; retries 429 / retryable 503 itself (1.0.9+)
print(sms["sms_code"])
# client.cancel(order["id"]) # cancel before the code arrives -> refunded
One-shot purchase + OTP:
from mrxsim import Client
with Client(country="egypt", service="whatsapp") as client:
result = client.buy_and_wait()
print(result["phone_number"], result["sms_code"])
Choosing a specific option (instead of operator="any") needs that option's quote from list_options():
row = options[0] # a stable MRXSIM option code such as "opt1" -- never a provider id
order = client.get_number(country="england", service="telegram", operator=row["operator"], quote=row["quote"])
A quote is short-lived (about two minutes) and bound to the price you saw; a missing, expired or stale quote fails with 409 before anything is charged.
Examples
Every script is in examples/. Run them from the repository root after pip install -e . (or pip install mrxsim) and exporting MRXSIM_API_KEY.
| Script | What it shows | Spends balance? | Command |
|---|---|---|---|
catalog_example.py |
Catalog drill-down: countries → services → options | No | python examples/catalog_example.py england telegram |
buy_number_example.py |
One purchase | Yes (needs --confirm-purchase) |
python examples/buy_number_example.py --confirm-purchase |
get_otp_example.py |
Wait for the SMS of an existing order (wait_for_sms, which retries 429 / retryable 503 itself since 1.0.9) |
No | python examples/get_otp_example.py <order_id> |
many_orders_example.py |
Wait for many orders at once with ONE shared client (threads; the client paces every poll) | No | python examples/many_orders_example.py <order_id> <order_id> ... |
status_polling_example.py |
Your own polling loop, honouring Retry-After (only needed when you want full manual control) |
No | python examples/status_polling_example.py <order_id> |
cancel_example.py |
Cancel an order, including the cooldown and refused-cancel cases | No (refunds) | python examples/cancel_example.py <order_id> --wait |
idempotent_purchase_example.py |
Purchase with an idempotency key and the right retry rules | Yes (needs --confirm-purchase) |
python examples/idempotent_purchase_example.py --country england --service telegram --confirm-purchase |
error_handling_example.py |
Every exception and how to handle it (runs fully offline) | No | python examples/error_handling_example.py |
direct_api_example.py |
The same API with plain requests, no SDK |
Yes only with --confirm-purchase |
python examples/direct_api_example.py england telegram |
rentals_pagination_example.py |
limit / offset pagination |
No | python examples/rentals_pagination_example.py |
mrxsim_client.py is a small legacy command-line demo (python mrxsim_client.py, reads config.json).
API surface
All paths are under /api/v1; authentication is the X-API-Key header (the catalog routes are public).
| Method | Endpoint | Client method |
|---|---|---|
GET |
/api/v1/balance |
Client.balance() |
GET |
/api/v1/sms/catalog |
Client.list_countries() |
GET |
/api/v1/sms/services?country=… |
Client.list_services() |
GET |
/api/v1/sms/operators?country=…&service=… |
Client.list_options() |
POST |
/api/v1/get_number |
Client.get_number() / buy_and_wait() |
GET |
/api/v1/get_sms?order_id=… |
Client.get_sms() / wait_for_sms() |
POST |
/api/v1/cancel |
Client.cancel() |
Rental Numbers, when enabled for your account (a disabled product answers 404, exactly like an unknown route):
| Method | Endpoint | Client method |
|---|---|---|
GET |
/api/v1/rental_countries |
client.rentals.list_countries() |
GET |
/api/v1/rental_plans?country=… |
client.rentals.list_plans() |
POST |
/api/v1/rental_quote |
client.rentals.quote() |
POST |
/api/v1/rental_purchase (requires Idempotency-Key) |
client.rentals.purchase() |
GET |
/api/v1/rental_list (limit, offset) |
client.rentals.list() |
GET |
/api/v1/rental_status?rental_id=… |
client.rentals.get() |
GET |
/api/v1/rental_messages?rental_id=… |
client.rentals.messages() |
POST |
/api/v1/rental_close (requires Idempotency-Key) |
client.rentals.close() |
POST |
/api/v1/rental_renew (requires Idempotency-Key; currently always refused) |
client.rentals.renew() |
Residential Proxies, when enabled for your account:
| Method | Endpoint | Client method |
|---|---|---|
GET |
/api/v1/proxy_locations |
client.proxies.list_locations() |
GET |
/api/v1/proxy_plans |
client.proxies.list_plans() |
POST |
/api/v1/proxy_quote |
client.proxies.quote() |
POST |
/api/v1/proxy_purchase (requires Idempotency-Key) |
client.proxies.purchase() |
GET |
/api/v1/proxy_list |
client.proxies.list() |
GET |
/api/v1/proxy_detail?proxy_id=… |
client.proxies.get() |
GET |
/api/v1/proxy_usage?proxy_id=… |
client.proxies.usage() |
GET |
/api/v1/proxy_credentials?proxy_id=… |
client.proxies.credentials() |
POST |
/api/v1/proxy_stop (requires Idempotency-Key) |
client.proxies.stop() |
Travel eSIM is not part of the API-key API (see "Travel eSIM" below).
Catalog and pagination
The catalog is a drill-down — list_countries() → list_services(country) → list_options(country=, service=) — and each call returns its full result; it does not paginate and does not use your purchase rate budget. list_options() lists every option MRXSIM has genuine backing for on that country/service, not just what is purchasable right now: a row can carry available: False / reliability: "Temporarily unavailable" and still be a real, normally priced product. Buying such a row fails cleanly with 409 (reason == "no_honorable_candidate"), never a silent substitution. Prices (price_usdt) are decimal strings: compare them numerically (Decimal), because "0.26" and "0.2600" are the same amount.
Endpoints that can grow take plain limit / offset query parameters and return a bare JSON array (no cursor, no envelope): today that is client.rentals.list(limit=100, offset=0) (limit 1–500). Track the offset yourself; a page shorter than limit is the last one. See examples/rentals_pagination_example.py.
Public order fields
Responses expose retail fields only. This is the complete PUBLIC_ORDER_FIELDS allowlist the client enforces (anything else is stripped client-side as defence in depth):
| Field | Meaning |
|---|---|
id |
MRXSIM order id |
phone_number |
Assigned number |
service / country |
Catalog codes |
status |
PROCESSING → PENDING → RECEIVED | EXPIRED | CANCELED |
price |
Retail USDT price charged |
sms_code |
OTP when received, else null |
substituted_operator |
The operator you asked for, if a different available one was delivered |
balance_usdt |
Wallet balance right after this call |
cancel_allowed |
false only inside a known cancel cooldown (not a promise that cancel will succeed) |
cancel_available_at / cancel_remaining_seconds |
When a cooldown ends, if one applies |
cancel_reason / cancel_state |
Machine-readable cooldown reason and state (ready, pending_cooldown) |
stars_refund_status / stars_refunded_amount |
Only for an order paid with Telegram Stars: its refund state after a cancel |
PROCESSING means the purchase is still confirming and no number exists yet; wait_for_sms() keeps polling through PROCESSING and PENDING. You can hold any number of orders open at once; each is tracked independently by its id.
Retry safety
Client never retries a purchase automatically — not on a timeout, not on a network error, not on a 503. A hidden automatic retry on a purchase call is how a client library causes a real second charge for one logical purchase. Instead it gives you what you need to retry safely when you choose to:
idempotency_keyonget_number()/buy_and_wait(): pass the same string across your own retry attempts (for example after catchingMrxsimTimeoutError). MRXSIM guarantees at most one real purchase per key (scoped to your account, up to 128 characters) and returns the original order on a repeat. Omit it for an independent purchase. Never reuse one key for a different request (409 idempotency_conflict).retry_afteronMrxsimRateLimitError(429) andMrxsimServiceUnavailableError(503): the server's realRetry-Aftervalue in seconds (Noneif absent). Prefer it over a fixed delay.
import time
from mrxsim import Client, MrxsimRateLimitError, MrxsimServiceUnavailableError
KEY = "my-own-stable-id-for-this-purchase"
client = Client()
try:
order = client.get_number(country="england", service="telegram", idempotency_key=KEY)
except (MrxsimRateLimitError, MrxsimServiceUnavailableError) as exc:
time.sleep(exc.retry_after or 2)
order = client.get_number(country="england", service="telegram", idempotency_key=KEY) # the SAME key
Reads never mutate anything and are safe to repeat. Since 1.0.9 the polling loop wait_for_sms() does that for you (see "Waiting for many codes"): it retries 429 and retryable 503 answers, request timeouts and connection resets, honouring Retry-After. A single direct call such as get_sms() or balance() still raises the first 429/503 immediately, with retry_after set. Purchases and cancels are never retried by the client. A complete, runnable version of these rules is examples/idempotent_purchase_example.py.
MrxsimAmbiguousPurchaseError — never retry this one with a new key
In the rare case where a charge or purchase may already have happened but MRXSIM could not yet record it, get_number() raises MrxsimAmbiguousPurchaseError (a subclass of MrxsimAPIError, so a generic except MrxsimAPIError also catches it — but it needs the opposite handling):
from mrxsim import Client, MrxsimAmbiguousPurchaseError
client = Client()
try:
order = client.get_number(country="england", service="telegram", idempotency_key=KEY)
except MrxsimAmbiguousPurchaseError as exc:
# Do NOT retry with a new idempotency_key: that risks a genuine second purchase. Retry with the EXACT SAME key,
# or hold and contact support with exc.purchase_intent_id.
print(exc.purchase_intent_id)
exc.purchase_intent_id is MRXSIM's own correlation id, safe to log. For rentals the equivalent is exc.rental_attempt_id.
Waiting for many codes (1.0.9)
wait_for_sms() is the one place the client retries on its own, because a poll is an idempotent read:
| Situation | Behaviour |
|---|---|
429 |
waits, then polls again. Never shorter than the server's guidance: the Retry-After header, or the public retry_after_seconds field when the header is missing or malformed (the larger value if both are usable); never shorter than 1 s; grows with repeated failures (bounded exponential backoff, cap 30 s); up to 1 s of jitter. If the server asks for longer than the time left before poll_timeout, the typed error is raised at once instead of polling early. pending_orders_limit is not retried |
503 |
retried only when the server marks it retryable (retryable: true / code: "at_capacity") or sends no machine-readable verdict at all (a plain gateway 503); same waiting rules. retryable: false or an unknown code is raised |
| request timeout, dropped or refused connection (connection reset, DNS failure) | retried with backoff; TLS/certificate and proxy errors, invalid URLs and redirect loops are raised at once |
401 / 403, 404, 422, other 4xx/5xx, ambiguous-purchase errors |
never retried: raised as the same typed exception as in 1.0.8 |
| terminal order state | CANCELED / EXPIRED raise MrxsimOrderError immediately; RECEIVED returns |
| sustained throttling | a per-client circuit breaker: after 30 throttling/backpressure answers within 60 s no poll is sent for 60 s (waiters sleep through the cooldown, then up to 1 s of random release jitter) |
| many orders at once | polls of all waiters of one client share one schedule, at least 60 / poll_budget_per_minute seconds apart (default 75/min = 0.8 s), so orders started together do not poll together and one key stays inside its documented read budget; a lone waiter polls exactly as in 1.0.8 (the one difference: 1.0.8 could poll up to one interval after poll_timeout, 1.0.9 makes one final poll exactly at the deadline). Pacing is per client: two clients on one key each get their own budget, so use one client per key; more than about poll_timeout / 0.8 simultaneous waiters cannot all be polled within poll_timeout |
Three clocks, kept apart: poll_interval is the pause between polls of one order; poll_timeout is the total time one wait_for_sms() call may take (pauses and retries included; no sleep ever runs past it, and a request already in flight can finish slightly after it); the client timeout is the per-request HTTP timeout (while polling it is capped by the time left, minimum 1 s). A purchase is unaffected: build the client you call get_number() with using timeout=120.0, because a purchase can take longer than the 30 s default. If the total deadline arrives while the last attempt failed transiently, that typed exception (MrxsimRateLimitError, MrxsimServiceUnavailableError, MrxsimTimeoutError, ...) is raised, because it is the real reason no answer arrived; otherwise MrxsimTimeoutError.
import threading
from mrxsim import Client
client = Client() # ONE client per process: pacing and the circuit breaker are per client
codes: dict[str, str] = {}
def wait(order_id: str) -> None:
codes[order_id] = client.wait_for_sms(order_id, poll_interval=3.0, poll_timeout=600.0)["sms_code"]
threads = [threading.Thread(target=wait, args=(oid,)) for oid in order_ids] # 5, 15 or 50 orders: fine
[t.start() for t in threads]; [t.join() for t in threads]
With many waiters each order is polled about every waiters × 0.8 seconds (15 orders ≈ every 12 s). An approved high-volume key can raise poll_budget_per_minute; pass None to disable the pacing. The SDK is synchronous: in an asyncio program call it through asyncio.to_thread(client.wait_for_sms, order_id) with one shared client. Pass retry_transient=False to the constructor for the exact 1.0.8 behaviour (the first 429/503/timeout is raised). get_sms(), balance(), the catalog calls, get_number() and cancel() keep their 1.0.8 behaviour: one attempt, typed exception. A runnable version is examples/many_orders_example.py.
Rentals lifecycle
import uuid
from mrxsim import Client
client = Client()
countries = client.rentals.list_countries()
plans = client.rentals.list_plans("US")
plan = next(p for p in plans if p["purchasable"]) # never offer a plan with purchasable=False
print(plan["refund_disclosure"]) # show this to your buyer before paying
quote = client.rentals.quote("US", plan["duration_type"], plan["duration_time"])
rental = client.rentals.purchase(quote["quote_id"], idempotency_key=str(uuid.uuid4()))
inbox = client.rentals.messages(rental["id"]) # a rental inbox can hold many SMS
if client.rentals.get(rental["id"])["can_close"]:
client.rentals.close(rental["id"], idempotency_key=str(uuid.uuid4()))
Statuses: ACTIVE, EXPIRED, CANCELED, CLOSED. A quote is short-lived and single-use. close() is only valid while can_close is true (409 otherwise); renew() is currently always refused (409, reason == "renewal_not_supported"). An unknown id and someone else's id are both 404. idempotency_key is required on purchase, close and renew (omitting it raises MrxsimConfigError).
Proxy lifecycle
import uuid
from mrxsim import Client
client = Client()
plans = client.proxies.list_plans()
plan = next(p for p in plans if p["available"])
quote = client.proxies.quote("US", "ROTATING", plan["plan_id"])
lease = client.proxies.purchase(quote["quote_id"], idempotency_key=str(uuid.uuid4()))
creds = client.proxies.credentials(lease["id"]) # only while ACTIVE or EXHAUSTED
usage = client.proxies.usage(lease["id"])
client.proxies.stop(lease["id"], idempotency_key=str(uuid.uuid4())) # revokes now, no auto-refund
Statuses: ACTIVE, EXHAUSTED, STOP_REQUESTED, STOPPING, STOPPED, EXPIRED. Credentials are never shown again once a lease is STOPPED or EXPIRED (409, reason == "not_active"). Treat username and password as secrets. idempotency_key is required on purchase and stop.
Travel eSIM
Travel eSIM is offered through the MRXSIM website and Mini App, not through the API-key Developer API, so the SDK has no working eSIM method: every client.esims.* call raises MrxsimUnsupportedError before any network request.
from mrxsim import Client, MrxsimUnsupportedError
try:
Client().esims.list_destinations()
except MrxsimUnsupportedError:
print("eSIM is not available through the API; use the website or Mini App.")
Ambiguous states
A purchase can end in a state MRXSIM is still reconciling. Never treat it as a failure, and never retry it with a new Idempotency-Key:
- HTTP
202, or503carryingpurchase_intent_id(numbers) orattempt_id(rentals), raisesMrxsimAmbiguousPurchaseError. Funds are reserved, not lost. - What to do: wait, then check
get_sms()/rentals.list()and your order history; retry only with the exact sameIdempotency-Key; otherwise contact support withexc.purchase_intent_idorexc.rental_attempt_id. - A number order in
PROCESSINGis the same idea: keep pollingget_sms().
Rate limits and retry guidance
Every call shares a system-wide ceiling, and each key has separate per-action budgets (purchases, reads, and for rentals/proxies quotes and close/stop); an approved high-volume key gets larger ones. The current numbers are in the Developer Docs under Rate limits. A 429 carries Retry-After, exposed as MrxsimRateLimitError.retry_after: wait at least that long.
Retry 429, 500 and 503 with backoff (except the ambiguous 503 above, which must reuse the same key). Never retry 401, 402, 404 or 422 unchanged. 409 means read exc.reason / exc.details["reason"] first (for example fetch a fresh quote). 202 is never something to retry: it means "wait and check".
Error semantics
| Status | Exception | Meaning |
|---|---|---|
| 401 / 403 | MrxsimAuthError |
missing or invalid key |
| 402 | MrxsimAPIError (exc.insufficient_balance) |
insufficient wallet balance |
| 404 | MrxsimAPIError |
not found, not yours, or product not enabled |
| 409 | MrxsimAPIError |
quote expired/invalid, action not allowed now, idempotency conflict; see exc.reason |
| 422 | MrxsimAPIError |
invalid request, or a stock-out (exc.stock_race) |
| 429 | MrxsimRateLimitError |
rate limited, see retry_after (wait_for_sms() retries it itself since 1.0.9) |
| 202 / 503 with an id | MrxsimAmbiguousPurchaseError |
ambiguous, see above |
| 503 | MrxsimServiceUnavailableError |
temporarily unavailable, retry with backoff (wait_for_sms() retries a retryable one itself since 1.0.9) |
| — | MrxsimTimeoutError |
an HTTP request timed out, or wait_for_sms() reached poll_timeout |
| — | MrxsimOrderError |
wait_for_sms() saw the order end as EXPIRED / CANCELED |
| — | MrxsimConfigError |
missing/invalid configuration or argument |
All of them inherit MrxsimError; those that represent a server response also inherit MrxsimAPIError and carry status_code and details. Error bodies never contain an upstream service name: routing is internal to MRXSIM and there is no parameter to choose or influence it. examples/error_handling_example.py runs every case offline.
Development
python -m venv .venv
# Windows: .venv\Scripts\activate
source .venv/bin/activate
pip install -e ".[dev]"
pytest
python -m build
The tests are fully offline (no network, no API key). See CHANGELOG.md for release notes.
Support
- Site: https://mrxsim.com
- Docs: https://mrxsim.com/docs
- Issues: GitHub
© MRXSIM · Secure SMS Infrastructure
Metadata
Release files for mrxsim 1.0.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mrxsim-1.0.9.tar.gz | 81.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mrxsim-1.0.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 123.4 kB
Release files / mrxsim-1.0.9.tar.gz
| Download URL | mrxsim-1.0.9.tar.gz |
|---|---|
| Size | 81.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
23bd70848a706c4a6697eff11f130541f5dee4178dd65c825cc83f66674876de
|
|
BLAKE2b-256 checksum How to use checksums |
8c7cc88b9992c7637a30357805a3b1900b8a1a69a52c97c9f0edfa95ecf76344
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|
Release files / mrxsim-1.0.9-py3-none-any.whl
| Download URL | mrxsim-1.0.9-py3-none-any.whl |
|---|---|
| Size | 42.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1335efe3dd7d1ee362a409112f973b92e8402d6db4995f900ed7cbfcb7aa06bd
|
|
BLAKE2b-256 checksum How to use checksums |
03645863d8910d0117f5fe9a11d2e95041ffd14ca7919bf5fc6b509bd94e7b95
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|