Skip to main content

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).

PyPI · 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 local config.json that 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

  1. Open https://mrxsim.com and create an account, then top up your wallet.
  2. Profile → Get API KEY (shown once). Keys start with mrxs_.
  3. 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_key on get_number() / buy_and_wait(): pass the same string across your own retry attempts (for example after catching MrxsimTimeoutError). 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_after on MrxsimRateLimitError (429) and MrxsimServiceUnavailableError (503): the server's real Retry-After value in seconds (None if 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, or 503 carrying purchase_intent_id (numbers) or attempt_id (rentals), raises MrxsimAmbiguousPurchaseError. Funds are reserved, not lost.
  • What to do: wait, then check get_sms() / rentals.list() and your order history; retry only with the exact same Idempotency-Key; otherwise contact support with exc.purchase_intent_id or exc.rental_attempt_id.
  • A number order in PROCESSING is the same idea: keep polling get_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

© 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)

Source distribution for mrxsim 1.0.9
File Size Uploaded
mrxsim-1.0.9.tar.gz 81.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mrxsim 1.0.9
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

1.0.9 This release

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page