expo-server-sdk-python
Python SDK for sending Expo push notifications from your own server, with chunking and receipt validation built in.
Repository Health
Technical Analysis
exponent-server-sdk-python (published to PyPI as exponent_server_sdk) is the community-maintained Python client for Expo’s push notification service. It wraps the HTTP/2 push API that Expo Go and standalone Expo/React Native apps rely on, giving backend developers a typed PushClient instead of hand-rolled requests calls against the raw API.
The library models the full push lifecycle as Python objects: a PushMessage namedtuple for building a notification payload, PushTicket and PushReceipt wrappers for interpreting Expo’s async two-step delivery confirmation, and a family of typed exceptions (DeviceNotRegisteredError, MessageTooBigError, MessageRateExceededError, InvalidCredentialsError) so callers can branch on specific push failures without parsing raw JSON error strings. It automatically chunks large batches into Expo’s documented limits (100 messages per publish call, 1000 IDs per receipt check) so a caller can push to arbitrarily large audiences with a single publish_multiple call.
What You Get
- PushClient - a session-based client for
POST /push/sendandPOST /push/getReceipts, with configurable host, API path, and timeout for testing or self-hosted Expo-compatible endpoints - PushMessage - a namedtuple covering every documented Expo push field (title, body, data, sound, badge, priority, ttl, channel_id, subtitle, mutable_content) with sane defaults so only
tois required - Automatic chunking -
publish_multipleandcheck_receipts_multiplesplit large batches into Expo’s 100-message / 1000-receipt limits without the caller managing pagination - Typed push errors -
DeviceNotRegisteredError,MessageTooBigError,MessageRateExceededError, andInvalidCredentialsErrormap directly onto Expo’s documented receipt error codes - force_fcm_v1 support - opt Android delivery into FCM’s V1 API independent of the credentials configured on Expo’s side
Common Use Cases
- Push notification backend - a Django/Flask/FastAPI server sending transactional or marketing pushes to a React Native app’s registered Expo push tokens
- Token hygiene jobs - a scheduled task that calls
check_receipts_multipleand deactivates any token that returnsDeviceNotRegisteredError - Bulk broadcast - fan-out messaging to a large user base in one
publish_multiplecall, relying on the SDK’s built-in chunking instead of writing batching logic - Retry-and-report pipelines - wrapping
publishcalls with a task queue (e.g. Celery) and error reporting service (the README documents a pyrollbar-based pattern) to retry transientConnectionError/HTTPErrorfailures
Under The Hood
Architecture
The entire package lives in a single module, exponent_server_sdk/__init__.py. PushClient is the sole entry point: its _publish_internal and _check_receipts_internal methods talk directly to Expo’s push endpoints over a shared requests.Session, while publish_multiple and check_receipts_multiple sit on top as thin chunking wrappers using itertools.islice. Data flows one way through immutable namedtuples — a caller builds a PushMessage, the client serializes it via get_payload(), and Expo’s JSON response is deserialized back into PushTicket/PushReceipt namedtuples rather than plain dicts. There is no dependency injection or plugin system; the only extension points are subclassing PushMessage to customize get_payload() and passing a preconfigured Session for auth headers. Because everything funnels through the two _internal methods, changing Expo’s request/response envelope would mean touching just those two functions, but there is also no abstraction layer between this SDK and Expo’s specific wire format.
Tech Stack
The library targets Python 3 and declares exactly two runtime dependencies in setup.py: requests for HTTP and six for the isinstance(token, six.string_types) check in is_exponent_push_token — a legacy Python 2/3 compatibility shim left over from the project’s 2016 origin. Packaging is classic setuptools/setup.py (no pyproject.toml, no build backend declaration, no pinned dependency versions), and there is no CI configuration, Dockerfile, or linter config anywhere in the repository.
Code Quality
No test files exist anywhere in the repository — there is no tests/ directory, no pytest/unittest usage, and no CI workflow to run them even if they existed. Error handling is deliberate and reasonably thorough for a small library: PushServerError and the PushTicketError subclasses carry structured context (response_data, errors, push_response) rather than swallowing failures, and validate_response() explicitly maps Expo’s documented error strings onto specific exception classes. Naming is consistent and mirrors Expo’s own API vocabulary (ticket vs. receipt, to/data/ttl fields), but there is no type-hinting, no docstring-generated docs, and no formatter/linter enforcing style.
API Design
The public surface is small and low-boilerplate: constructing a PushClient() with no arguments works out of the box against Expo’s production host, and sending a notification is a single publish(PushMessage(to=..., body=...)) call. Optional fields default to None via PushMessage.__new__.__defaults__, so callers only ever specify what they need. The main ergonomic gap is documentation depth — docstrings exist on most classes and explain the what, but usage guidance beyond the README’s one code sample is limited, and callers must read Expo’s separate push-notification docs to fully understand receipt semantics.