Metadata-Version: 2.4
Name: payexa
Version: 0.5.0
Summary: Python SDK for Payexa (pexn.ir), the Iranian card-to-card payment gateway with automatic confirmation: API v2 payments, signed callbacks and verify, sync and async
Author: PAYEXA Team
Maintainer: PAYEXA Team
License-Expression: MIT
Project-URL: Homepage, https://pexn.ir
Project-URL: Documentation, https://pexn.ir/doc
Project-URL: Tools, https://pexn.ir/tools
Project-URL: Downloads, https://pypi.org/project/payexa/#files
Keywords: payexa,pexn,card-to-card,iran,payment-gateway,payments,telegram-bot,sdk,async
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3,>=2.31
Provides-Extra: async
Requires-Dist: httpx<1,>=0.27; extra == "async"
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == "dev"
Requires-Dist: twine<7,>=5; extra == "dev"
Dynamic: license-file

# PAYEXA Python SDK

Official Python SDK for the PAYEXA payment API (v2): create a payment, send the buyer to it, verify it.

[Payexa](https://pexn.ir) (pexn.ir) is an Iranian card-to-card payment gateway with automatic confirmation. The buyer pays with an ordinary card-to-card transfer in their own banking app. The bank's deposit SMS reaches Payexa through the Payexa Uploader Android app, and the payment is matched by a unique amount and confirmed automatically. Payexa works with websites, WooCommerce, Telegram and Bale bots, Telegram channel shops and payment links.

The package provides a small, typed interface for Python sites and bots, such as a Django, Flask or FastAPI site or a Telegram bot, with sync and async clients.

پی‌اکسا درگاه پرداخت کارت به کارت با تأیید خودکار است. خریدار با برنامهٔ بانک خودش کارت به کارت می‌کند و پرداخت با مبلغ یکتا خودکار تأیید می‌شود. این کتابخانه با API نسخهٔ ۲ کار می‌کند: ساخت پرداخت، بازگشت خریدار، اعلان امضاشده و تأیید پرداخت، برای سایت‌ها و ربات‌های پایتونی. مستندات فارسی: https://pexn.ir/doc

- Website: https://pexn.ir
- API documentation: https://pexn.ir/doc
- Plugins and SDKs: https://pexn.ir/tools

## Installation

```bash
pip install payexa
```

With the async client:

```bash
pip install "payexa[async]"
```

Python 3.10 or newer.

## How it works

1. Your server creates a payment with `create_payment` and keeps one value: `payment_id`.
2. The buyer pays on the Payexa payment page (`payment_url`) or in the Payexa payment bot (`telegram_url`, `bale_url`).
3. Payexa calls your `callback_url` twice: a signed server-to-server notification (POST) as soon as the payment is paid, and the buyer's return (GET).
4. In both cases, call `verify(payment_id)`. Deliver the order only when `first_verification` is `True`.

Amounts are in rial (whole numbers, at least 10,000). Each payment stays open for 30 minutes.

## Website

```python
from payexa import Payexa

px = Payexa(api_key="YOUR_API_KEY")

payment = px.create_payment(
    amount=5_000_000,  # rial
    order_id="INV-1024",  # your own order number
    callback_url="https://shop.example.com/payexa/callback",
    description="One-month subscription",  # optional, shown on the payment page
    metadata={"user_id": 88},  # optional, returned in verify and notifications
)

# Keep payment.payment_id with the order, then send the buyer to the payment page:
redirect(payment.payment_url)
```

Creating a payment again with the same `order_id` and amount returns the same payment (`payment.created` is `False`), so a double click does not create two payments.

The callback route answers both the notification and the buyer's return (Flask):

```python
from flask import request

from payexa import PayexaAPIError, PayexaSignatureError


def deliver(payment_id: str) -> bool:
    try:
        result = px.verify(payment_id)
    except PayexaAPIError:  # PAYMENT_NOT_PAID, PAYMENT_EXPIRED, ...
        return False
    if result.first_verification:  # True for exactly one verify of this payment
        fulfill_order(result.order_id)
    return True


@app.route("/payexa/callback", methods=["GET", "POST"])
def payexa_callback():
    if request.method == "POST":  # signed server-to-server notification
        try:
            notification = px.parse_notification(request.get_data(), request.headers.get("X-Payexa-Signature"))
        except PayexaSignatureError:
            return "", 400
        if notification.is_paid:
            deliver(notification.payment_id)
        return "", 200

    back = px.parse_return(request.args)  # the buyer's return: ?payment_id=..&order_id=..&status=..
    if back.has_payment_id and deliver(back.payment_id):
        return "Payment successful"
    return "Payment not completed"
```

`status` in the buyer's return (`paid`, `expired` or `canceled`) is only a hint. The result of `verify` is final.

## Telegram or Bale bot

Give the buyer the payment bot link as a button. Put your own bot link in `return_url`; when the buyer comes back, your bot receives `/start <payment_id>`.

```python
payment = px.create_payment(
    amount=5_000_000,
    order_id="TG-88-1024",
    return_url="https://t.me/ShopBot",  # Bale: https://ble.ir/ShopBot
    # callback_url="https://bot.example.com/payexa/callback",  # only if the bot has a web server
)
pay_link = payment.telegram_url or payment.payment_url  # Bale: payment.bale_url

# When the buyer returns, the message text is "/start <payment_id>":
payment_id = px.parse_start(message.text)
if payment_id and deliver(payment_id):
    ...
```

A bot without a web server can also ask for the state of its open payments with `px.get_payment(payment_id)`.

## Async

```python
from payexa import AsyncPayexa


async def checkout() -> str:
    async with AsyncPayexa(api_key="YOUR_API_KEY") as px:
        payment = await px.create_payment(
            amount=5_000_000,
            order_id="INV-1024",
            callback_url="https://shop.example.com/payexa/callback",
        )
        return payment.payment_url
```

`AsyncPayexa` has the same methods as `Payexa`, as coroutines: `create_payment`, `get_payment`, `find_payment` and `verify`. `parse_return`, `parse_start`, `parse_notification` and `verify_signature` are plain methods.

## Methods

| Method | API | Returns |
|---|---|---|
| `create_payment(amount=, order_id=, callback_url=, return_url=, description=, metadata=)` | `POST /v2/payments` | `Payment` |
| `get_payment(payment_id)` | `GET /v2/payments/{payment_id}` | `Payment` |
| `find_payment(order_id)` | `GET /v2/payments?order_id=` | `Payment` (the latest of that order) |
| `verify(payment_id, amount=None)` | `POST /v2/payments/{payment_id}/verify` | `Verification` |
| `parse_return(params)` | the buyer's return (GET) | `PaymentReturn` |
| `parse_notification(body, signature)` | the notification (POST), signature checked | `Notification` |
| `verify_signature(body, signature)` | `X-Payexa-Signature` | `bool` |
| `parse_start(text)` | `/start <payment_id>` in a bot | `payment_id` or `None` |

`Payment` has `payment_id`, `order_id`, `status` (`pending`, `paid` or `expired`), `amount`, `payable_amount` (the exact amount the buyer transfers), `payable_amount_toman`, `expires_at`, `payment_url`, `telegram_url`, `bale_url`, `card`, `description`, `metadata`, and from `get_payment` / `find_payment` also `paid_at`, `verified` and `verified_at`. `Verification` adds `first_verification`. Every model keeps the full answer in `raw`.

## Errors

- `PayexaValidationError`: invalid input, raised before any request is sent.
- `PayexaTransportError`: network or timeout error.
- `PayexaAPIError`: PAYEXA answered with an error. It has `status_code`, `code`, `message` (Persian, for display), `fields` (for `VALIDATION_ERROR`) and `retry_after` (for `RATE_LIMITED`).
- `PayexaSignatureError`: the notification signature is missing, wrong or older than 5 minutes.

Common `code` values: `PAYMENT_NOT_PAID`, `PAYMENT_EXPIRED`, `AMOUNT_MISMATCH`, `PAYMENT_NOT_FOUND`, `ORDER_ID_CONFLICT`, `CALLBACK_NOT_ALLOWED`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `MERCHANT_INACTIVE`, `MERCHANT_TEMPORARILY_INACTIVE`, `INSUFFICIENT_WALLET_BALANCE`, `NO_ACTIVE_CARD`, `RATE_LIMITED`. The full list is in the documentation.

`callback_url` and `return_url` must be on one of your approved sites or bots in the Payexa panel, otherwise `CALLBACK_NOT_ALLOWED`.

## Sandbox

The sandbox runs the whole flow without real money, with the public key `PAYEXA`:

```python
from payexa import Payexa, SANDBOX_BASE_URL

px = Payexa(api_key="PAYEXA", base_url=SANDBOX_BASE_URL)
```

## API v1 (earlier clients)

`PayexaClient` and `AsyncPayexaClient` still work with API v1 (`/v1/payment/request` and `/v1/payment/verify`) and keep their behaviour, so existing integrations do not need to change. New integrations should use `Payexa` / `AsyncPayexa`. One integration uses one API version: a payment created with v1 is verified with v1.

```python
from payexa import PayexaClient

with PayexaClient(base_url="https://pay.pexn.ir", api_key="YOUR_API_KEY") as client:
    payment = client.request_payment(amount=600000, callback_url="https://merchant.example.com/payexa/callback")
    # keep payment.order_id and payment.amount_unique_token, send the buyer to payment.start_pay_url
    verification = client.verify_payment(order_id=payment.order_id, token=payment.amount_unique_token)
```

## Release

For maintainers, the release workflow is documented in `RELEASE_CHECKLIST.md`.
