Getting Started#

Setup#

Create a Platega client with your merchant credentials. Credentials are available in your Platega dashboard under Settings.

from aioplatega import Platega

client = Platega(merchant_id="your-merchant-id", secret="your-secret-key")

Tip

Always use the async context manager to ensure proper cleanup of the connection pool:

async with Platega(merchant_id="...", secret="...") as client:
    ...

Creating a transaction#

from aioplatega import Platega, PaymentMethodInt, PaymentDetails

async with Platega(merchant_id="...", secret="...") as client:
    result = await client.create_transaction(
        payment_method=PaymentMethodInt.SBP_QR,
        payment_details=PaymentDetails(amount=500.0, currency="RUB"),
        description="Order #42",
        return_url="https://example.com/success",
        failed_url="https://example.com/fail",
    )

    print(result.transaction_id)  # UUID
    print(result.status)          # "PENDING"
    print(result.redirect)        # URL to redirect user to

Note

The transactionId is generated by the Platega system automatically — do not pass an id field in the request.

Danger

Shops in certain categories must send the payer identifier as metadata.userId. Where that requirement applies, omitting it disables antifraud protection and can get the shop suspended. Ask your Platega manager whether it applies to yours.

from aioplatega import PaymentMetadata

result = await client.create_transaction(
    payment_method=PaymentMethodInt.SBP_QR,
    payment_details=PaymentDetails(amount=500.0, currency="RUB"),
    metadata=PaymentMetadata(user_id="your-internal-user-id", user_name="Ivan"),
)

A plain dict works too, if your shop needs keys beyond the documented userId and userName.

Tip

payment_method accepts any integer, not only a named PaymentMethodInt member. The enum names the documented methods, but ids 1–9 are P2P and a merchant is enabled for whichever their contract covers.


Checking transaction status#

result = await client.get_transaction_status("transaction-uuid")

print(result.status)           # "PENDING" | "CONFIRMED" | "CANCELED" | "CHARGEBACKED"
print(result.payment_details)  # PaymentDetails or None

Getting exchange rate#

Note

get_rate() calls /rates/payment_method_rate, which is documented in the older GitBook but is absent from the current API reference. It is live; treat the omission as a sign that it may be withdrawn without notice.

rate = await client.get_rate(
    payment_method=2,
    currency_from="RUB",
    currency_to="USDT",
)

print(rate.rate)         # float, e.g. 0.0105
print(rate.updated_at)   # datetime

Getting conversions#

conversions = await client.get_conversions(
    from_date="2026-01-01T00:00:00Z",
    to_date="2026-12-31T23:59:59Z",
    page=0,
    size=50,
)

for op in conversions:
    print(op.source_amount, op.source_currency, "->", op.target_amount, op.target_currency)
    print(op.exchange_rate, op.operation_date, op.processed_transactions_count)

if conversions.pagination:
    print(f"Total: {conversions.pagination.total}")

Note

The response is iterable over its operations. Earlier releases exposed content and total_elements, which no live response has ever contained — the call silently returned an empty list. It reads operations and pagination now.


Error handling#

from aioplatega.exceptions import (
    PlategaError,
    PlategaBadRequestError,
    PlategaUnauthorizedError,
    PlategaRateLimitError,
    PlategaNetworkError,
    PlategaValidationError,
)

try:
    result = await client.create_transaction(...)
except PlategaValidationError as e:
    print(f"Bad arguments, nothing was sent: {e}")
except PlategaBadRequestError as e:
    print(f"Bad request: {e.message}, status={e.status_code}")
except PlategaUnauthorizedError:
    print("Invalid credentials")
except PlategaRateLimitError:
    print("Rate limited — back off and retry")
except PlategaNetworkError as e:
    print(f"Network error: {e}")
except PlategaError as e:
    print(f"Unexpected error: {e}")

Tip

Everything this library raises derives from PlategaError, so a single except PlategaError is enough if you do not need to tell the cases apart.

Exception hierarchy
PlategaError
├── PlategaAPIError
│   ├── PlategaBadRequestError          (400)
│   ├── PlategaUnauthorizedError        (401)
│   ├── PlategaForbiddenError           (403)
│   ├── PlategaNotFoundError            (404)
│   ├── PlategaConflictError            (409)
│   ├── PlategaUnprocessableEntityError (422)
│   ├── PlategaRateLimitError           (429)
│   └── PlategaServerError              (5xx)
├── PlategaNetworkError
├── PlategaValidationError
└── ClientDecodeError

Callback payload#

Platega authenticates a callback by echoing your own X-MerchantId and X-Secret back at you — there is no signature over the body. Verify them before trusting anything:

from aioplatega.exceptions import PlategaValidationError

@app.route("/callback", methods=["POST"])
def callback():
    try:
        payload = client.verify_callback(request.headers, request.get_data())
    except PlategaValidationError:
        return "", 401

    if payload.status == PaymentStatus.CONFIRMED:
        mark_paid(payload.payload)
    return "", 200

Warning

Compare the credentials in constant time. A plain == against your secret leaks it byte by byte to anyone who can measure the response. verify_callback uses hmac.compare_digest; if you roll your own check, do the same.

verify_callback is framework-agnostic — pass whatever header mapping and raw body your framework exposes. It is also available as a standalone function for code that has no client at hand:

from aioplatega import verify_callback

payload = verify_callback(headers, body, merchant_id="...", secret="...")

For the subscription callbacks, whose fields are PascalCase, pass the matching model:

from aioplatega import SubscriptionChargeCallback

charge = client.verify_callback(headers, body, model=SubscriptionChargeCallback)

If you only need to parse a body you have already authenticated:

from aioplatega import CallbackPayload

payload = CallbackPayload.model_validate(request_json)

print(payload.id)              # transaction UUID
print(payload.status)          # "CONFIRMED" | "CANCELED" | "CHARGEBACKED"
print(payload.amount)          # float
print(payload.payment_method)  # int

Warning

Callback requirements:

  • Your endpoint must respond with HTTP 200 within 60 seconds

  • Up to 3 retries at 5-minute intervals on failure

  • Configure the URL in dashboard: Settings → Callback URLs

Rules for the URL itself:

  • HTTPS only — plain HTTP is rejected

  • A public IP address or domain name; private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8), localhost and loopback addresses are all rejected

  • A valid certificate from a trusted CA; self-signed certificates are rejected

Note

The published schema lists only CONFIRMED and CANCELED for the callback status, while the same page’s prose adds CHARGEBACKED for refunds. CallbackPayload.status is therefore a plain string, so any of the three — or a future fourth — parses.

Subscriptions send two further callbacks, with PascalCase field names:

from aioplatega import SubscriptionChargeCallback, SubscriptionStatusCallback

charge = SubscriptionChargeCallback.model_validate(request_json)
print(charge.subscription_id, charge.next_charge_at)

change = SubscriptionStatusCallback.model_validate(request_json)
print(change.id, change.status)  # id is the subscription id here

Command pattern#

You can also build and dispatch method objects directly:

from aioplatega.methods import CreateTransaction
from aioplatega import PaymentMethodInt, PaymentDetails

method = CreateTransaction(
    payment_method=PaymentMethodInt.CARD_ACQUIRING,
    payment_details=PaymentDetails(amount=1000.0, currency="RUB"),
)

result = await client(method)

Recurring SBP subscriptions#

from aioplatega import SubscriptionInterval, SubscriptionPaymentDetails

sub = await client.create_subscription(
    payment_details=SubscriptionPaymentDetails(
        amount=500,
        currency="RUB",
        interval=SubscriptionInterval.MONTH,
    ),
    description="Premium подписка",
)
print(sub.redirect)        # send the payer here to confirm the mandate
print(sub.transaction_id)  # this is the subscription id — keep it

Important

transaction_id in the create response is the subscription id, not a transaction id. Every later subscription call takes it.

detail = await client.get_subscription(sub.transaction_id)
page = await client.list_subscriptions(status="Active", page=0, size=20)
await client.cancel_subscription(sub.transaction_id)

Note

Subscriptions take SubscriptionPaymentDetails, not PaymentDetails: the charge period is part of the payment details and is required. amount is an integer here, matching the subscription schema.

Note

On the way back, status and interval_unit are typed str | int because the API returns them as numbers in the list response and as words in the single-subscription response.


Refunds#

Check first, then cancel — the check reports the USDT cost as well as whether it is possible at all:

check = await client.check_cancel_supported(transaction_id)
if check.supported:
    result = await client.cancel_transaction(transaction_id)
    if result.manual_control_required:
        print("needs manual handling:", result.message)
else:
    print("cannot cancel:", check.block_reason)

Balances and exports#

for balance in await client.get_balances():
    print(balance.currency, balance.amount, balance.frozen_balance)

link = await client.export_transactions_csv(
    statuses=["CONFIRMED"],
    from_date="2026-01-01",
    to_date="2026-02-01",
    time_zone_id="Europe/Moscow",
)
print(link.url)

rows = await client.export_transactions_json(statuses=["CONFIRMED"])
for row in rows:
    print(row.record_id, row.amount, row.currency_code)

The CSV and Excel exports return a download link; the JSON export returns the rows inline.


Payouts#

Payouts authenticate differently from everything above: a separate secret, issued once in the dashboard, signs each request with HMAC-SHA256. The feature is off by default on a Platega account and enabled on request.

from aioplatega import PayoutClient

async with PayoutClient(merchant_id="...", secret="payout-secret") as payouts:
    cards = await payouts.get_cards()
    result = await payouts.create_card_payout(
        card_id=cards[0].card_id,
        amount_rub=1500,
        idempotency_key="order-42-payout",
    )
    print(result.status, result.amount_usdt_debited)

Note

The payout secret is shown once when generated and Platega cannot recover it. Resetting it in the dashboard invalidates the old key immediately — every request still signed with it starts failing authentication.

Warning

Pass your own idempotency_key whenever a retry has to be safe. Without one a fresh key is generated per call, so a retried request counts as a second payout. Amounts are limited to 1000–87500 RUB, checked client-side before anything is sent.