mailsocket — Python SDK
The official Python client for the mailsocket v1 REST API. Per-test email inboxes, message retrieval, and — the whole point — wait for the OTP in one line. For testing apps you own or are authorized to test. Not for creating accounts on services you don't control.
import time
from mailsocket import Client
client = Client("ms_live_...") # your API key from the dashboard
inbox = client.create_inbox(label="signup") # -> {"id": "inbox_...", "address": "..."}
started = time.time() # take the cutoff BEFORE the trigger
# use inbox["address"] in your app's signup form, then:
result = client.wait_for_otp(inbox["id"], since=started) # blocks up to 60s
print(result.otp) # "123456"
print(result.confidence) # 0.95
Take started just before you trigger the email and pass it as since: a
code that lands before wait_for_otp starts is still caught, and an older
code in a reused inbox is not.
No email handy? client.send_test_code(inbox_id) drops a sample code into
the inbox so you can see the whole loop work:
started = time.time()
client.send_test_code(inbox["id"]) # -> {"message_id": "msg_...", "sent_at": "..."}
result = client.wait_for_otp(inbox["id"], since=started)
print(result.otp) # the sample code
No polling loop. No regex over the email body. wait_for_otp long-polls the
server (which already knows how to extract the code) and hands back a
deterministic result with a confidence score.
Install
pip install mailsocket
Zero runtime dependencies — pure standard library (urllib). Python 3.9+.
API
client = Client(api_key, base_url="https://dash.mailsocket.app/api/v1")
| Method | Returns |
|---|---|
create_inbox(label=None) |
dict — the inbox (id, address, ...) |
list_inboxes(limit=None, cursor=None) |
Page — .data list + .next_cursor / .has_more |
get_inbox(inbox_id) |
dict — inbox plus message_count_month, webhook_configured |
delete_inbox(inbox_id) |
None |
list_messages(inbox_id, has_otp=None, subject_contains=None, sender=None, ...) |
Page — sender maps to the API's from filter |
get_latest(inbox_id) |
dict — newest message |
get_message(message_id) |
dict — full message |
send_test_code(inbox_id) |
dict — message_id, sent_at of a sample OTP email put straight into the inbox (no mail server; 5 per inbox / 20 per account per hour) |
The moat
result = client.wait_for_otp("inbox_abc123", timeout=60, min_confidence=0.0, since=None)
result = client.wait_for_link("inbox_abc123", timeout=60, since=None)
result = client.wait("inbox_abc123", timeout=60, min_confidence=0.0, since=None) # otp OR link
With a cutoff taken before the trigger (the usual case):
started = time.time()
# trigger the email here
result = client.wait_for_otp("inbox_abc123", since=started)
All three return a WaitResult and take keyword-only arguments after
inbox_id. timeout/min_confidence/since shown above are the defaults —
call with no extra arguments for the common case.
| Method | Keyword args (all optional, keyword-only) | Returns |
|---|---|---|
wait_for_otp(inbox_id, ...) |
timeout=60, min_confidence=0.0, since=None |
WaitResult |
wait_for_link(inbox_id, ...) |
timeout=60, since=None |
WaitResult |
wait(inbox_id, ...) |
timeout=60, min_confidence=0.0, since=None |
WaitResult (otp OR link) |
since=None (the default) omits the param, so the server uses its own
default: the request start time — a code that arrives during this call is
caught, and a stale code already in a reused inbox is not. Pass since=0 to
restore the old "any message already in the inbox" behaviour (useful right
after create_inbox, where there's nothing stale to match), or a message id
/ timestamp to continue after a specific point.
WaitResult exposes .otp, .confidence, .magic_link (and .link as an
alias), plus the full .message dict. str(result) is the OTP (or the link).
Semantics:
- Each HTTP call blocks server-side for up to 25s (
timeoutis clamped to[1, 25]); the SDK re-calls until the overalltimeout(seconds, default 60) elapses. 200→ the matching message.204→ nothing yet, re-call immediately.429→ honoursRetry-Afterand retries within the deadline.404→ raises.- On overall deadline →
WaitTimeout.
Errors
MailsocketError (base) with subclasses:
AuthError— HTTP 401 (bad/missing key).NotFound— HTTP 404 (also an unknown, deleted or foreign inbox insend_test_code).RateLimited— HTTP 429, carries.subcode(rate_limited,too_many_wait_requests,wait_capacity) and.retry_after.WaitTimeout— no matching message within the overall deadline.
send_test_code on a disabled inbox raises the base MailsocketError with
code="inbox_disabled" and status=409. Over its hourly quota it raises
RateLimited; .retry_after says how many seconds until it frees up.
Develop
cd python
python -m pytest -q
Tests make no external or live-API requests. Most monkeypatch
urllib.request.urlopen with a scripted responder, and a few start a real local
http.server on 127.0.0.1 to check actual request behaviour, e.g. that an
invalid id never reaches the network.
Changelog
- 0.3.2 — docs only: README positions mailsocket as email testing for apps you own or are authorized to test. No code change.
- 0.3.1 — the
wait*helpers no longer miss a message that lands between two long-poll calls. When you pass nosince, the first call still omits it (the server uses the request start), and every following call sends the cutoff the server reported in theMailsocket-Wait-Sinceresponse header of the previous call. With an older server that sends no such header, behaviour is unchanged. An explicitsinceis used as before on every call. - 0.3.0 — new
Client.send_test_code(inbox_id)(POST /inboxes/{id}/test-code): puts one sample OTP email into your own inbox without going through the mail server, and returns{"message_id", "sent_at"}. README samples now takestarted = time.time()before the trigger and passsince=started. - 0.2.0 — BEHAVIOUR CHANGE:
wait_for_otp/wait_for_link/waitnow defaultsince=None, which omits the param so the server uses the request start time, instead of the oldsince=0("any message already in the inbox"). The old default silently returned a STALE OTP/link from a reused inbox. Passsince=0explicitly to keep the old behaviour. - 0.1.3 —
_segnow rejects an empty id or a bare./..id with aMailsocketError(code="invalid_id") before any request is made, matching the TypeScript SDK's contract.
Metadata
Release files for mailsocket 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mailsocket-0.3.2.tar.gz | 19.8 kB | Without provenance | Details for mailsocket-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mailsocket-0.3.2-py3-none-any.whl | Python 3 | none | any | Details for mailsocket-0.3.2-py3-none-any.whl |
Total release size: 31.5 kB
Release files / mailsocket-0.3.2.tar.gz
| Download URL | mailsocket-0.3.2.tar.gz |
|---|---|
| Size | 19.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9242e6aa1e68f957875ff276285a0108ed97a2af7f7e4419328562aaafa7b950
|
|
BLAKE2b-256 checksum How to use checksums |
96db6c7396c07bcdb6c77cd0dd5f89746f6699ce6119776ecf4e90b5b0b6e05a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / mailsocket-0.3.2-py3-none-any.whl
| Download URL | mailsocket-0.3.2-py3-none-any.whl |
|---|---|
| Size | 11.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a5fe138c425d18a2cb877502bdb29ddceb6ec25eec6a56aec6d120b1b7a9f9b0
|
|
BLAKE2b-256 checksum How to use checksums |
575d0031baa105b336f7bc45b3967ba9c6e2c5e7b343d0843bb533db90fb1edc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for mailsocket 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.