Metadata-Version: 2.4
Name: mpay-uapi-sdk
Version: 1.0.0
Summary: Official Python SDK for the mPay uAPI (wallet, cardholder and card management)
Author: mPay
License: MIT
Project-URL: Homepage, https://github.com/fundway/mpay_uapi_sdk/python
Project-URL: Documentation, https://docs.mpay.cards
Project-URL: Issues, https://github.com/fundway/mpay_uapi_sdk/issues
Keywords: mpay,sdk,api,credit-card,virtual-card,wallet,hmac
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3,>=2.25
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Dynamic: license-file

# mpay-uapi-sdk

Official Python SDK for the mPay uAPI, covering the **wallet**, **holder**
(cardholder), and **card** endpoint groups, with built-in **HMAC-SHA256**
request signing.

## Installation

```bash
pip install mpay-uapi-sdk
```

## Quickstart

```python
from mpay_uapi_sdk import MpayUapiClient, MpayAPIError

client = MpayUapiClient(
    access_key="ak_xxxxxxxx",   # Your API access key
    secret_key="sk_xxxxxxxx",   # Your API secret key (keep this confidential)
    base_url="https://uapi.mpay.cards",  # Optional, defaults to production
)

# 1. Wallet
balance = client.wallet.get_balance()
print(balance)  # {'balance': 100, 'avail_balance': 100, 'lock_balance': 0}

txs = client.wallet.get_transactions(direction="in", page=1, limit=20)

# 2. Cardholder
holder = client.holder.get_holder_info()
client.holder.set_holder_info(first_name="James", last_name="Brown")

# 3. Card
products = client.card.get_products()
cards = client.card.list(status="ACTIVE")
card = client.card.get_info(card_id="2078022262790127618")
sensitive = client.card.get_sensitive(card_id="2078022262790127618")

# Asynchronous operations (create / recharge)
op = client.card.create_card(product_id=products[0]["product_id"])
if op["status"] == "PROCESSING":
    result = client.wait_for_card_operation(op["operation_id"])
    print(result)

try:
    client.card.recharge_card(card_id="2078022262790127618", amount=50)
except MpayAPIError as e:
    print(f"API error: {e.code} - {e.message}")
```

You can also use a `with` block so the underlying HTTP session is closed
automatically:

```python
with MpayUapiClient(access_key="ak_xxx", secret_key="sk_xxx") as client:
    print(client.wallet.get_wallet_balance())
```

## Endpoint reference

### Wallet — `client.wallet`

| Method | Endpoint | Description |
| --- | --- | --- |
| `get_wallet_balance()` | `GET /v1/wallet/balance` | Get the wallet balance |
| `get_wallet_transactions(direction=None, page=1, limit=20)` | `GET /v1/wallet/transactions` | Get wallet transaction records |
| `get_deposit_chains()` | `GET /v1/deposit/chains` | Get supported blockchain networks |
| `get_deposit_options(group_by="network")` | `GET /v1/deposit/options` | Get available deposit options |
| `get_deposit_address(chain_id)` | `GET /v1/deposit/address` | Get the on-chain deposit address |
| `get_deposit_transactions(chain_id=None, page=1, limit=20)` | `GET /v1/deposit/transactions` | Get deposit transaction records |

### Holder — `client.holder`

| Method | Endpoint | Description |
| --- | --- | --- |
| `get_holder_info()` | `GET /v1/holder/info` | Get cardholder information |
| `set_holder_info(first_name, last_name)` | `POST /v1/holder/set` | Update cardholder information |

### Card — `client.card`

| Method | Endpoint | Description |
| --- | --- | --- |
| `get_products()` | `GET /v1/card/products` | Get the list of card products |
| `get_statuses()` | `GET /v1/card/statuses` | Get the list of card statuses |
| `get_cards(status=None)` | `GET /v1/card/list` | Get the list of cards |
| `get_card_info(card_id)` | `GET /v1/card/info` | Get card information |
| `get_card_sensitive(card_id)` | `GET /v1/card/sensitive` | Get sensitive card data (PAN/CVV/expiry) |
| `get_card_transactions(card_id, page=1, limit=20)` | `GET /v1/card/transactions` | Get card transaction records |
| `remark_card(card_id, remark)` | `POST /v1/card/remark` | Set a card remark |
| `create_card(product_id)` | `POST /v1/card/create` | Create a new card (async) |
| `recharge_card(card_id, amount)` | `POST /v1/card/recharge` | Recharge a card (async) |
| `get_card_operation_status(operation_id)` | `GET /v1/card/operation/status` | Query the status of an async operation |

For asynchronous endpoints such as `create_card` and `recharge_card`, use
`client.wait_for_card_operation(operation_id)` to poll until the operation
reaches a terminal state (`SUCCESS`, `FAILED`, etc.).

## Authentication (HMAC-SHA256)

The SDK automatically attaches the following signing headers to every
request:

| Header | Description |
| --- | --- |
| `X-Api-Key` | Your API access key (public identifier) |
| `X-Timestamp` | Unix timestamp (seconds) when the request was signed |
| `X-Nonce` | A random, per-request string used for replay protection |
| `X-Signature` | The hex-encoded HMAC-SHA256 signature |

**String-to-sign** format:

```
METHOD\n
PATH\n
TIMESTAMP\n
NONCE\n
CANONICAL_QUERY_STRING
```

- `METHOD`: the upper-cased HTTP method, e.g. `GET`
- `PATH`: the request path without the query string, e.g. `/v1/wallet/balance`
- `CANONICAL_QUERY_STRING`: all request parameters, URL-encoded, sorted
  lexicographically by key, and joined with `&` (e.g. `a=1&b=2`). An empty
  parameter set produces an empty string.

Final signature = `hex(HMAC_SHA256(secret_key, string_to_sign))`.

The server should recompute the signature with the same rules and compare
it against `X-Signature`, and should also validate that
`X-Timestamp` is within an acceptable clock-skew window (e.g. 5
minutes) to prevent replay attacks.

> Note: the header names above (`X-Api-Key`, `X-Timestamp`, `X-Nonce`,
> `X-Signature`) match the server's requirements. If the server ever
> changes the required header names or the string-to-sign layout, update
> the constants and `build_string_to_sign()` in `mpay_uapi_sdk/auth.py`
> accordingly — the rest of the SDK does not need to change.

## Error handling

All exceptions raised by this SDK inherit from `MpayError`:

- `MpayAPIError`: the API returned a business-level error (exposes `code`,
  `message`, `http_status`, `response`)
- `MpayNetworkError`: a network-level failure (timeout, connection error, etc.)
- `MpayConfigError`: invalid client configuration
- `MpaySignatureError`: a signing-related error

```python
from mpay_uapi_sdk import MpayAPIError, MpayNetworkError

try:
    client.card.get_info(card_id="xxx")
except MpayAPIError as e:
    print(e.code, e.message, e.http_status)
except MpayNetworkError as e:
    print("network problem:", e)
```

## Development & publishing

```bash
# Install development dependencies
pip install -e ".[dev]"

# Run the test suite
pytest

# Build distribution artifacts
python -m build

# Upload to PyPI (configure ~/.pypirc or TWINE_USERNAME/TWINE_PASSWORD first)
python -m twine upload dist/*
```

## License

MIT
