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),localhostand loopback addresses are all rejectedA 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.