mrxsim
Official Python client for MRXSIM.COM — purchase virtual numbers and retrieve SMS OTPs for every catalog app/social service (Telegram, WhatsApp, Google, Instagram, Discord, Snapchat, Other (SMS), and more).
Brand: MRXSIM · Domain: https://mrxsim.com · Package:
mrxsim
Security first
- No hardcoded API keys in library source or examples.
- Prefer environment variable
MRXSIM_API_KEY. - Or load a local
config.jsonthat is gitignored. - Never commit live keys. Revoke immediately if exposed.
Install
pip install mrxsim
From this repository (editable):
pip install -e .
Quick start (environment variable)
export MRXSIM_API_KEY="mrxs_your_key_here" # Linux / macOS
# setx MRXSIM_API_KEY "mrxs_your_key_here" # Windows (new shell)
from mrxsim import Client
with Client() as client:
# Discover the catalog — no country/service needed to browse.
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=options[0]["operator"], # a stable MRXSIM option code — never a provider ID
idempotency_key="my-own-stable-id-for-this-purchase", # optional, see "Retry safety" below
)
print(order["phone_number"], order["id"])
sms = client.wait_for_sms(order["id"])
print(sms["sms_code"])
# client.cancel(order["id"]) # if you need to cancel before the OTP arrives
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"])
Quick start (config file)
cp config.example.json config.json
# edit config.json → set api_key, country, service
from mrxsim import Client
client = Client.from_config("config.json")
order = client.get_number()
sms = client.wait_for_sms(order["id"])
client.close()
MRXSIM_API_KEY overrides api_key in the file when set.
Get your API key
- Open https://mrxsim.com and create an account.
- Top up with Crypto (USDT / supported networks).
- Profile → Get API KEY (shown once at create/regenerate).
- Export
MRXSIM_API_KEYor paste into gitignoredconfig.json.
API surface
| 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() |
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 |
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 Developer API: it is offered through the
signed-in website / Mini App only, so client.esims.* raises
MrxsimUnsupportedError before any network call. See "Travel eSIM" below.
Catalog discovery (list_countries/list_services/list_options) is public MRXSIM
data — no purchase, no debit — and does not consume your purchase-tier rate budget.
An operator value from list_options() is a stable, MRXSIM-owned option code
(e.g. "opt1", "opt2" for a service with more than one genuinely distinct price
tier) — never an upstream provider identifier — pass it straight to get_number().
list_options() lists every option MRXSIM has ever had genuine backing for on that
country/service, not just what's purchasable right now — a row can carry
available: False / reliability: "Temporarily unavailable" and still be a real,
normally priced product; don't assume every row is instantly purchasable, and don't
drop one from your own UI/cache just because it's temporarily unavailable (MRXSIM
doesn't either — it resumes automatically once real backing returns). Buying such a
row fails cleanly with a 409 (reason == "no_honorable_candidate"), never a silent
substitution or a 500. Compare price_usdt numerically, not as a string — the same
price can render with a different number of decimal places between calls (e.g.
"0.26" vs. "0.2600").
Header on every call:
X-API-Key: YOUR_KEY
Public order fields (zero-knowledge)
Successful responses expose retail fields only — aligned with the MRXSIM
server OrderPublicOut contract. This is the complete PUBLIC_ORDER_FIELDS
allowlist the client enforces client-side (see "Retry safety" and the
changelog below for the newer fields in context):
| Field | Meaning |
|---|---|
id |
MRXSIM order UUID |
phone_number |
Assigned number |
service / country |
Catalog codes |
status |
Order status |
price |
Retail USDT price charged |
sms_code |
OTP when received |
substituted_operator |
Set when Smart Auto-Fallback delivered a different (never worse) operator than requested |
balance_usdt |
Wallet balance remaining after this purchase |
cancel_allowed |
Whether this order is currently cancelable at all |
cancel_available_at |
ISO timestamp cancellation becomes available, if in cooldown |
cancel_remaining_seconds |
Server-computed seconds left in a cancel cooldown, if any |
stars_refund_status / stars_refunded_amount |
Only for an order funded through Telegram Stars: the state and amount of its refund after a cancel; otherwise null |
Order status values: PROCESSING (purchase confirming, no number yet) then
PENDING (number issued, waiting for the code) then RECEIVED | EXPIRED |
CANCELED. wait_for_sms() keeps polling through PROCESSING and PENDING.
Internal operational metrics and upstream routing identifiers are not part of the public API. The client also sanitizes responses client-side if any such fields ever appear (defense in depth).
Docs: https://mrxsim.com/docs
Retry safety
Client never automatically retries get_number() — not on a timeout, not
on a network error, not on a 503. A hidden automatic retry on a purchase call
is exactly how a client library accidentally causes a real second charge for
one logical purchase, so this client simply doesn't do it. What it gives you
instead, so you can retry safely when you choose to:
idempotency_keyonget_number()— pass the same string across your own retry attempts (e.g. after catchingMrxsimTimeoutError) and MRXSIM guarantees at most one real purchase for that key, returning the original order on a repeat instead of buying a second number. Omit it for a normal, independent purchase.MrxsimRateLimitError.retry_after(429) andMrxsimServiceUnavailableError.retry_after(503, MRXSIM's own capacity backpressure — distinct from a per-key rate limit) — both carry the server's realRetry-Aftervalue in seconds (Noneif the response didn't include one). Prefer it over a fixed client-side delay:
import time
from mrxsim import Client, MrxsimRateLimitError, MrxsimServiceUnavailableError
client = Client()
try:
order = client.get_number(idempotency_key="my-own-stable-id-for-this-purchase")
except (MrxsimRateLimitError, MrxsimServiceUnavailableError) as exc:
if exc.retry_after:
time.sleep(exc.retry_after)
order = client.get_number(idempotency_key="my-own-stable-id-for-this-purchase")
else:
raise
Plain read calls (get_sms(), status polling inside wait_for_sms()) are
naturally safe to retry on your own — they never mutate anything — so this
client doesn't add speculative auto-retry logic there either; keep your own
retry loop as simple or as sophisticated as your application needs.
MrxsimAmbiguousPurchaseError — never retry this one with a new key
For the rare case where a real charge/vendor purchase may already have
happened but MRXSIM's own record of it failed to save, get_number() raises
MrxsimAmbiguousPurchaseError (a subclass of MrxsimAPIError, so a
generic except MrxsimAPIError still catches it — but it needs the
opposite handling from every other error above):
from mrxsim import Client, MrxsimAmbiguousPurchaseError
client = Client()
try:
order = client.get_number(idempotency_key="my-own-stable-id-for-this-purchase")
except MrxsimAmbiguousPurchaseError as exc:
# Do NOT retry with a new idempotency_key — that risks a genuine second
# real purchase. Retry with the EXACT SAME key, or hold and contact
# support with exc.purchase_intent_id.
order = client.get_number(idempotency_key="my-own-stable-id-for-this-purchase")
exc.purchase_intent_id is MRXSIM's own internal correlation id (never
provider-derived, safe to log) — keep it if you need to contact support
instead of retrying immediately.
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.
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.
Travel eSIM
Travel eSIM is offered through the MRXSIM website and Mini App, not through the
API-key Developer API, so there is no eSIM method in this SDK. Purchase can be
temporarily unavailable; on the signed-in eSIM surface that is signalled by a
404 (product switched off, identical to an unknown route), a 503 with
reason provider_unavailable, or a 503 daily-capacity reason. A 202 with
reason == "ambiguous_purchase_pending_reconciliation" means the outcome is
still being confirmed: your wallet stays debited until it is resolved.
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()/ 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; each key also has per-action budgets
(purchase, reads, and for rentals/proxies quote and close/stop), and an approved
high-volume key gets a larger multiple. A 429 carries Retry-After, exposed as
MrxsimRateLimitError.retry_after: wait at least that long. Retry 429, 500
and 503 with backoff; never retry 401, 402, 404 or 422 unchanged. 409
means read exc.details["reason"] first (for example fetch a fresh quote).
This client never auto-retries a purchase.
Error semantics
| Status | Exception | Meaning |
|---|---|---|
| 401 | MrxsimAuthError |
missing or invalid key |
| 402 | MrxsimAPIError |
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 details["reason"] |
| 422 | MrxsimAPIError |
invalid request, or a stock-out (details["stock_race"]) |
| 429 | MrxsimRateLimitError |
rate limited, see retry_after |
| 202 / 503 with an id | MrxsimAmbiguousPurchaseError |
ambiguous, see above |
| 503 | MrxsimServiceUnavailableError |
temporarily unavailable, retry with backoff |
Error bodies never contain an upstream service name. Routing is internal to MRXSIM: there is no parameter to choose or influence the upstream source.
Changelog
Note on Rental Numbers / Travel eSIM / Residential Proxies (added
2026-09-29): the dated entries below track this SDK's ORIGINAL
single-product (Temporary Numbers) iteration history. client.rentals,
client.esims, and client.proxies were each added later as fully
separate product domains — see their own sections above ("Rental
Numbers", "Travel eSIM", "Proxy lifecycle") for what each one does and
how to use it. None of the three ever got its own dated changelog entry
when it landed, and this SDK has never been published to PyPI in any
state that lacked any of them — there is no real prior published version
a consumer could have been missing them from — so backfilling precise
per-domain "added in version X" entries here would just be fabricating
history this package never actually had. What matters for a real
consumer is simple: version 1.0.8 (the current, not-yet-published
candidate) includes all four product domains (SMS, Rentals, eSIM,
Proxy) as documented above, in full.
Docs-only clarification (2026-09-04, MRXSIM Major Rework Program, Agent
20, "Docs Final Update" pass) — no code or version change. This pass
runs after the owner-directed Telegram/Website "always clickable" UX
corrections and the final red-team revalidation, and re-verified this
package's docstrings against the actual current server code rather than
re-stating the prior pass's wording. Those two UX corrections were a
Telegram-bot/Website presentation-layer fix only — they never touched
this Developer API's wire contract, which had no client-side gate to
begin with. One small wording fix made: get_number()'s docstring
described a stale quote as "no longer honorable" (an internal-jargon
phrase); reworded to plain English ("no longer valid"). The literal,
correct reason value the server actually returns
(no_honorable_candidate) is unchanged and still documented verbatim.
Docs-only clarification (2026-09-04, MRXSIM Major Rework Program, Agent
20) — no code or version change. list_options()'s docstring and the
catalog-discovery note above were corrected/clarified: the previous
reliability example values ("High", "Unrated") did not match the
labels the server actually sends ("Available" / "Limited availability"
/ "Temporarily unavailable"), and neither this README nor the docstring
explained that an available: False row is a real, permanently listed
product rather than one about to vanish — the "always-visible catalog"
contract. Also added: a note to compare price_usdt numerically, never as
a string (the same price can render with a different decimal-place count
between calls). See docs/rework_agent20_docs_sdk_findings.md in the main
MRXSIM repository for the full reasoning; nothing here changes this
package's behavior, so the version stays 1.0.6.
1.0.8
cancel_reason/cancel_statenow correctly returned byget_sms()andcancel(), not justget_number()— real bug found and fixed server-side (2026-09-28): the cancellation-policy fields added in 1.0.6 were only ever wired intoPOST /get_number's response;GET /get_smsandPOST /cancelkept returningnull/nullfor both fields regardless of the order's real state, so a caller pollingget_sms()got a different (wrong) answer than callingget_number()on the same order — a real parity break against this file's own "a developer must never see a different answer than the retail UI would" principle. Both fields were already in this client's ownPUBLIC_ORDER_FIELDSallowlist (added in 1.0.6) and needed no client-side change once the server started sending them consistently — this version bump exists because that server-side fix genuinely changed what real API responses contain, and the version number had never been updated to reflect it.- Trimmed
wait_for_sms()'s internal terminal-failure status set to match the real, documented public status vocabulary exactly (PROCESSING/PENDING/RECEIVED/EXPIRED/CANCELED) — real gap found (Phase 9 API-freeze audit): the set also listed"CANCELLED"(double-L),"TIMEOUT", and"FAILED", none of which the server has ever actually assigned to a real order. Those were unreachable dead branches, not a wider real vocabulary — no behavior change for any real order, since those statuses never occurred. MrxsimUnsupportedErrornow exported from the package root (mrxsim.MrxsimUnsupportedError) — real gap found (external-developer validation pass): the public docs already name this as what everyclient.esims.*method raises, but it was only reachable via the undocumented internal pathfrom mrxsim.esims import MrxsimUnsupportedError.client.esims/EsimsAPIitself remains intentionally unexported (there is no live eSIM endpoint to use it against).MrxsimAuthErroris now a realMrxsimAPIErrorsubclass, with a realstatus_code— real gap found (external-developer validation pass): every other exception representing an actual HTTP response (MrxsimRateLimitError,MrxsimServiceUnavailableError,MrxsimAmbiguousPurchaseError) already inherited fromMrxsimAPIErrorand carriedstatus_code;MrxsimAuthErrordid not, so a reasonableexcept MrxsimAPIError as e: handle(e.status_code)catch-all for any HTTP error silently never caught a 401/403.status_codeis401or403for a real server response,Nonefor this client's own pre-flight placeholder-key check (no HTTP request made). No import path changed —from mrxsim import MrxsimAuthErrorstill works exactly as before.- Omitting
idempotency_keyentirely (not just passing"") onrentals.purchase/close/renew/proxies.purchase/stopnow raisesMrxsimConfigError, not a bare PythonTypeError— real gap found (external-developer validation pass): these five methods requiredidempotency_keyas a keyword-only argument with no default, so Python's own call machinery raisedTypeErrorbefore this SDK's code — and its documentedMrxsimConfigError— ever ran, breaking the "always catch a documentedMrxsimXxxError" idiom this package otherwise upholds everywhere else. Now typedstr | None = None; omitting it or passing""both raise the sameMrxsimConfigError. - Two documentation corrections on the live Developer Docs page (not an
SDK code change, no version-relevant behavior change):
list_options()'snamefield is a permanent per-tier label assigned once and never recomputed, not "assigned fresh from current rank" as the page previously (and incorrectly) said — the live behavior was already correct, only the description was wrong; and the Rental Numbersrental_quoteexample now explicitly notes it currently returns409 insufficient_stockgiven today's genuinely empty inventory, rather than looking like a copy-paste mistake. - User-Agent bumped to
mrxsim-python/1.0.8.
1.0.7
MrxsimAmbiguousPurchaseError— real gap found and closed: this exception class has existed inmrxsim.exceptionssince the ambiguous -purchase safety work landed, but was never re-exported from the top-levelmrxsimpackage or listed in__all__— a caller following this README's own documented pattern (from mrxsim import MrxsimAmbiguousPurchaseError) would get anImportError. Now correctly exported; see "MrxsimAmbiguousPurchaseError — never retry this one with a new key" below for the full retry-safety guidance this exception exists for. No behavior change —_raise_for_status()was already raising this exception type correctly; only its discoverability was broken.- User-Agent bumped to
mrxsim-python/1.0.7.
1.0.6
Client.balance()— real gap found and closed: the server'sGET /api/v1/balanceendpoint has existed since 2026-09-01, but this client never exposed a dedicated method for it. Returns{"balance_usdt": "..."}; cheap, idempotent, does not consume your purchase-tier rate budget.PUBLIC_ORDER_FIELDSwidened to match the real serverOrderPublicOutexactly — real bug found and fixed:balance_usdt,cancel_allowed,cancel_available_at, andcancel_remaining_seconds(all real fields the server has sent on everyget_number()/get_sms()/cancel()response since 2026-09-01) were being silently stripped by this client's own sanitizer before this fix — contradictingget_sms()'s andcancel()'s own documented return values. Every prior version back to 1.0.3 was affected. If you're on an older version, upgrade — no code changes needed on your end, you'll simply start receiving fields the server was already sending.- User-Agent bumped to
mrxsim-python/1.0.6.
1.0.3
get_number(idempotency_key=...)— optional, forwarded as anIdempotency-Keyheader; a retry with the same key (and the same country/service/operator) returns the original order instead of a second purchase. Omitted by default — this client never generates or reuses a key on your behalf.MrxsimRateLimitError/newMrxsimServiceUnavailableErrornow expose a realretry_after(seconds) parsed from the server'sRetry-Afterheader,Noneif absent.MrxsimServiceUnavailableErroris new — raised on503(MRXSIM's own capacity backpressure, distinct from the per-key429).- New "Retry safety" section above — documents that this client never automatically retries a purchase call, by design.
list_countries()/list_services()/list_options()— real catalog discovery, added 2026-08-31 (this version was never published while this gap existed, so it's folded into 1.0.3 rather than bumping to 1.0.4). Calls MRXSIM's existing public catalog endpoints directly — same canonical data Website/Mini App/Bot render, zero provider identity ever included.cancel(order_id)— added 2026-08-31, alongside a real server-side developer-API cancel endpoint (there was none before). Reuses the same proven cancel/refund logic the web/bot surface already uses — idempotent, provider-neutral errors, refunds only on a confirmed provider outcome.- User-Agent bumped to
mrxsim-python/1.0.3.
1.0.2
- Sanitize client responses to strip internal operational metrics and upstream routing identifiers.
- Harden public documentation for white-label / zero-knowledge API alignment.
- User-Agent bumped to
mrxsim-python/1.0.2.
1.0.1
- Document and enforce zero-knowledge public order fields.
- User-Agent bumped to
mrxsim-python/1.0.1.
Examples
export MRXSIM_API_KEY="…"
python examples/buy_number_example.py
python examples/get_otp_example.py <order_id>
Development
python -m venv .venv
# Windows: .venv\Scripts\activate
source .venv/bin/activate
pip install -e ".[dev]"
pytest
python -m build
Support
- Site: https://mrxsim.com
- Docs: https://mrxsim.com/docs
- Issues: GitHub
© MRXSIM · Secure SMS Infrastructure
Metadata
Release files for mrxsim 1.0.8
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.8.tar.gz | 61.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mrxsim-1.0.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 101.7 kB
Release files / mrxsim-1.0.8.tar.gz
| Download URL | mrxsim-1.0.8.tar.gz |
|---|---|
| Size | 61.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c218a6acbb7707d2d2fcedfaa3debb1776abcdfbe9902a4e98d176239da27576
|
|
BLAKE2b-256 checksum How to use checksums |
f6165787174120dfb1a39f72b09794080f202e744ddee46bc10a0344250db6c4
|
| 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.8-py3-none-any.whl
| Download URL | mrxsim-1.0.8-py3-none-any.whl |
|---|---|
| Size | 39.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
660fab7011afe7a7e63cab006bb0b9c125287491dcebf5610db3d20820bcb706
|
|
BLAKE2b-256 checksum How to use checksums |
53d500e9dd5e2ab38541e83ebcb8a975c97e1a9d662ea5fc2b41a21c267bbe5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|