Official Python client for the MilkyWay Payments API (/payments/v1).
Project description
MilkyWay Payments SDK for Python
Official Python client for the MilkyWay Payments API (/payments/v1) — the
partner-facing API that banks use to initiate, quote, track, and cancel
cross-bank payments.
Batteries included:
- Keycloak client-credentials auth with in-memory token caching, automatic
refresh ~30s before expiry, single-flight acquisition, and a one-shot
refresh-and-replay on
401. - Retries (exponential backoff + jitter via
urllib3) on transient failures (5xx, 408, network), with deterministic errors (400/401/402/404) never retried. - Typed models & exceptions — money is
decimal.Decimal(parsed losslessly from JSON numbers), status is anIntEnum, and each HTTP error maps to a specific exception type. - Idempotency-key passthrough on
pay(apaywithout a key is sent exactly once — never auto-retried). - A
wait_for_completionpolling helper with exponential backoff and a timeout budget. - Full type hints and a
py.typedmarker.
Install
pip install milkyway-payments
The distribution is milkyway-payments; the import package is milkyway_payments.
Requires Python 3.8+. Bring your Keycloak client_id / client_secret and you're
ready.
Quick start
from decimal import Decimal
from milkyway_payments import (
MilkywayPaymentsClient, MilkywayOptions, PrecheckRequest, PayRequest,
)
client = MilkywayPaymentsClient(MilkywayOptions(
base_url="https://milkyway.stage.planet9.ae",
token_url="https://keycloak.ac8o.planet9.ae/realms/planet9-stage/protocol/openid-connect/token",
client_id="your-client-id", # issued to your institution
client_secret="your-client-secret",
))
# 1. Is the recipient bank's service online?
client.healthcheck("bank-beta", "card-payout")
# 2. Quote the payment (FX markup + commission applied here).
quote = client.precheck(PrecheckRequest(
third_party_id_debit="bank-beta",
service_id="card-payout",
recipient_id="recipient-9999",
amount_credit=Decimal("100.00"),
currency_credit="USD",
))
print(f"Rate {quote.rate}, debit {quote.amount_debit} {quote.currency_debit}, commission {quote.commission}")
# 3. Initiate the payment. Pass an idempotency_key so retries are safe.
import uuid
transaction_id = client.pay(PayRequest(
third_party_id_debit="bank-beta",
service_id="card-payout",
sender_id="sender-0001",
recipient_id="recipient-9999",
amount_credit=Decimal("100.00"),
currency_credit="USD",
data={"passport": "AA1234567"},
), idempotency_key=str(uuid.uuid4()))
# 4. Poll until the payment reaches a terminal status.
result = client.wait_for_completion(transaction_id)
print(f"Final status: {result.status.name}")
MilkywayPaymentsClient is also a context manager (with MilkywayPaymentsClient(...) as client:)
and exposes .close() to release its HTTP sessions.
The data field
Each service requires extra per-partner fields (sender name, document number,
birthday, …) in the data dictionary. Which keys are required depends on your
service_id and the recipient bank — look them up in the Услуги registry. The
server validates data against the service's JSON Schema during precheck, so a
missing field is rejected before any money moves. Omit data (leave it None)
and it is not sent on the wire.
Errors
All API errors raise a subclass of MilkywayApiError (carrying status_code,
message, and the raw response_body):
| HTTP | Exception | Meaning |
|---|---|---|
| 400 | MilkywayValidationError |
Bad request (invalid amount, missing field, unresolvable FX rate). |
| 401 | MilkywayAuthError |
Token missing/invalid (also raised if token acquisition fails). |
| 402 | MilkywayExposureBlockedError |
Payment would breach a block-action exposure limit. |
| 404 | MilkywayNotFoundError |
Transaction not found or not owned by your institution. |
| 5xx | MilkywayServiceUnavailableError |
API or downstream recipient unavailable (retried automatically first). |
Retries & idempotency
Transient failures are retried automatically with exponential backoff + jitter
(tunable via MilkywayOptions.max_retries / retry_base_delay). pay is only
auto-retried when you supply an idempotency_key — without one, a retry could
create a duplicate payment, so the SDK sends it exactly once.
Configuration
| Option | Default | Purpose |
|---|---|---|
base_url |
— (required) | Payments API base URL. |
token_url |
— (required) | Keycloak token endpoint. |
client_id / client_secret |
— (required) | Your institution's credentials. |
scope |
None |
Optional OAuth scope. |
token_refresh_skew |
30.0 (s) |
Refresh this long before token expiry. |
request_timeout |
30.0 (s) |
Per-attempt request timeout. |
max_retries |
3 |
Max transient-failure retries. |
retry_base_delay |
0.5 (s) |
Base delay for exponential backoff. |
Developing
python -m venv .venv && source .venv/bin/activate
pip install -e ".[test]"
pytest
Releasing
Releases are fully automated by semantic-release
on every push to main:
- Conventional commits are analysed (
feat:→ minor,fix:/perf:→ patch,!/BREAKING CHANGE→ major). No releasable commits → no release. - The computed version is written into
pyproject.tomland__init__.py, the distribution is built withpython -m build, and a GitHub release +vX.Y.Ztag are created. - A separate workflow, triggered on the published GitHub release, uploads the built artifacts to PyPI via Trusted Publishing (OIDC — no long-lived API token stored anywhere).
One-time PyPI Trusted Publisher setup (maintainers)
Before the first publish can succeed, register the trusted publisher on PyPI:
- Sign in to https://pypi.org and go to Your projects → milkyway-payments → Settings → Publishing (or, before the project exists, Account settings → Publishing → Add a pending publisher).
- Add a GitHub Actions trusted publisher with:
- Owner:
bankplanet9 - Repository:
milkyway-python-sdk - Workflow name:
publish.yml - Environment:
pypi
- Owner:
- In the GitHub repo, create an Environment named
pypiand set the repository variablePUBLISH_ENABLEDtotrue. The publish job is gated onvars.PUBLISH_ENABLED == 'true', so until the trusted-publisher policy and this variable exist, releases tag and build but skip the PyPI upload (they never fail).
No API tokens or passwords are stored anywhere — PyPI mints a short-lived OIDC credential at publish time.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file milkyway_payments-1.0.1.tar.gz.
File metadata
- Download URL: milkyway_payments-1.0.1.tar.gz
- Upload date:
- Size: 10.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8e4b9ebcea5d7a764c4b55fb76b588a91808dfa3f921631371ef17d6715573e
|
|
| MD5 |
b07b19041d9caf5630b3756830a28d6a
|
|
| BLAKE2b-256 |
5bb4c53f8dfda6843906a2af40e702dcdb5a043223d3aaa83baeaffffa80dba2
|
Provenance
The following attestation bundles were made for milkyway_payments-1.0.1.tar.gz:
Publisher:
publish.yml on bankplanet9/milkyway-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
milkyway_payments-1.0.1.tar.gz -
Subject digest:
a8e4b9ebcea5d7a764c4b55fb76b588a91808dfa3f921631371ef17d6715573e - Sigstore transparency entry: 1912183905
- Sigstore integration time:
-
Permalink:
bankplanet9/milkyway-python-sdk@2be64b3105557edbdef9b55adb54cef355e2ad17 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bankplanet9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2be64b3105557edbdef9b55adb54cef355e2ad17 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file milkyway_payments-1.0.1-py3-none-any.whl.
File metadata
- Download URL: milkyway_payments-1.0.1-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0d56f8e3974fbdc5cafb0715dcfd6f3291e61b3fda6e3dbbdb930e75d94d467
|
|
| MD5 |
362f08481e12934592a5144315ce2a14
|
|
| BLAKE2b-256 |
e2045ac4c26464380a9e7fd9d3b748d335858298cd0aef982f18a4af612ea8d0
|
Provenance
The following attestation bundles were made for milkyway_payments-1.0.1-py3-none-any.whl:
Publisher:
publish.yml on bankplanet9/milkyway-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
milkyway_payments-1.0.1-py3-none-any.whl -
Subject digest:
c0d56f8e3974fbdc5cafb0715dcfd6f3291e61b3fda6e3dbbdb930e75d94d467 - Sigstore transparency entry: 1912184054
- Sigstore integration time:
-
Permalink:
bankplanet9/milkyway-python-sdk@2be64b3105557edbdef9b55adb54cef355e2ad17 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/bankplanet9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2be64b3105557edbdef9b55adb54cef355e2ad17 -
Trigger Event:
workflow_dispatch
-
Statement type: