Async Python client for the PayTR payment APIs (iFrame, callback, refund, reporting).
Project description
paytr-python
A small, typed, async Python library for the PayTR
payment APIs — with optional ready-to-use FastAPI routes. The client is built
on aiohttp (or bring your own httpx); FastAPI + pydantic are only pulled in
by the optional [fastapi] extra, so import paytr stays dependency-light.
Covers most of the PayTR surface. Only the buyer-facing purchase flow is
exposed as routes. Everything else — refund, status, reporting, payment links,
stored cards, BIN / installment queries and direct/recurring payment — is
backend-only: a PayTRClient method with no route, called from your own
trusted server-side code (never the browser). Build your own user-scoped,
authenticated endpoint on top if buyers need self-service.
| Capability | PayTR endpoint | Client method | Route |
|---|---|---|---|
| iFrame token (STEP 1, new design v2, card + Havale/EFT) | /odeme/api/get-token |
create_iframe_token() |
POST /paytr/pay |
| Callback verification (STEP 2) | your URL | verify_callback() |
POST /paytr/callback |
| Refund (full / partial) | /odeme/iade |
refund() |
backend only |
| Status query | /odeme/durum-sorgu |
status() |
backend only |
| Transaction-detail report | /rapor/islem-dokumu |
transaction_detail() |
backend only |
| Payment statement / summary | /rapor/odeme-dokumu |
payment_statement() |
backend only |
| Payment-detail report | /rapor/odeme-detayi |
payment_detail() |
backend only |
| Create / delete payment link | /odeme/api/link/{create,delete} |
create_payment_link() / delete_payment_link() |
backend only |
| BIN lookup | /odeme/api/bin-detail |
bin_detail() |
backend only |
| Installment rates | /odeme/taksit-oranlari |
installment_rates() |
backend only |
| Stored cards (list / delete) | /odeme/capi/{list,delete} |
list_cards() / delete_card() |
backend only |
| Direct / recurring payment | /odeme |
direct_payment() |
backend only |
Plus the official error-code tables (paytr.describe(scope, code)).
Not implemented: pre-authorization — its wire-level spec isn't public (contact PayTR for the integration doc). It is deliberately left out rather than guessed, since this is payment-signing code.
Install
pip install "paytr-python[fastapi]" # client + FastAPI routes
pip install paytr-python # client only (no FastAPI dependency)
Credentials (merchant_id, merchant_key, merchant_salt) are on the BİLGİ /
INFORMATION page of the PayTR Merchant Panel. Keep the key and salt secret.
Test cards (only valid with test_mode=True; name and expiry are free-form,
CVV 000): 4355084355084358, 5406675406675403, 9792030394440796 — exp e.g.
12/30, holder "PAYTR TEST". The iFrame test form injects these for you; you'll
need them for direct_payment testing.
1. Ready-to-use FastAPI routes
from fastapi import FastAPI
from paytr import PayTRClient
from paytr.fastapi import include_paytr_routes, CallbackData
app = FastAPI()
client = PayTRClient(
merchant_id="123456", merchant_key="...", merchant_salt="...",
test_mode=True, # use PayTR test cards; flip to False in production
)
async def on_payment(data: CallbackData) -> None:
# Verified callback. Idempotent: act once per merchant_oid (PayTR retries).
if data.is_success:
... # credit the order
else:
print("payment failed:", data.error_message)
include_paytr_routes(app, client, on_payment=on_payment) # mounts everything
That mounts, under /paytr (configurable via prefix=):
POST /paytr/pay create a V2 iFrame token (STEP 1)
POST /paytr/callback payment result callback (STEP 2, PayTR -> you)
GET /paytr/ok, /paytr/fail default buyer redirect targets
That's the whole buyer purchase flow — nothing more. Every merchant-only
operation (refund, status, reporting, links, stored cards,
BIN / installment, direct payment) is deliberately not routed; call
client.refund() / client.status() / client.create_payment_link() etc. from
your own trusted backend (see §2).
Nothing is mounted at / — everything stays under the prefix so it won't clash
with your app's routes. Optional knobs: prefix, ok_url, fail_url. Use
create_paytr_router(...) instead if you want the APIRouter to include
yourself.
POST /paytr/pay body
{
"email": "buyer@example.com",
"user_name": "Jane Buyer",
"user_address": "Somewhere 1",
"user_phone": "05551112233",
"basket": [{"name": "Item 1", "unit_price": 18.0, "quantity": 1}],
"currency": "TL",
"lang": "tr"
}
Amounts are in major units (e.g. 18.0 ₺). The library handles the ×100
conversion, basket encoding, and HMAC signing. Response:
{"status": "success", "merchant_oid": "...", "token": "...", "iframe_url": "..."}.
Render the iframe with that iframe_url (and the PayTR resizer script).
Security
The basket in POST /paytr/pay is client-supplied, so /pay will sign
whatever prices the browser sends. Never trust those prices — derive them
from a server-side catalog by merchant_oid (or product id).
For defence in depth, pass an async get_expected_amount(merchant_oid) that
returns the order's expected total in minor units (kuruş). A successful
callback whose total_amount doesn't match is rejected with HTTP 400, before
your on_payment runs:
async def get_expected_amount(merchant_oid: str) -> int | None:
order = await orders.get(merchant_oid) # your server-side record
return order.total_kurus if order else None # None → skip the check
include_paytr_routes(
app, client, on_payment=on_payment, get_expected_amount=get_expected_amount
)
Callbacks are HMAC-verified with a constant-time comparison out of the box,
so a forged or replayed notification never reaches on_payment.
2. The client (framework-agnostic)
from paytr import PayTRClient, iframe_html
client = PayTRClient(merchant_id="...", merchant_key="...", merchant_salt="...")
result = await client.create_iframe_token(
merchant_oid="ORDER123",
email="buyer@example.com",
payment_amount="34.56",
user_ip="1.2.3.4",
user_name="Jane Buyer",
user_address="Somewhere 1",
user_phone="05551112233",
user_basket=[("Item 1", "18.00", 1), ("Item 2", "16.56", 1)],
merchant_ok_url="https://shop.example.com/ok",
merchant_fail_url="https://shop.example.com/fail",
)
html = iframe_html(result["token"])
# Other backend calls
await client.refund(merchant_oid="ORDER123", return_amount="11.90")
await client.status("ORDER123")
await client.transaction_detail(start_date="2021-02-02 00:00:00", end_date="2021-02-04 23:59:59")
await client.payment_statement(start_date="2022-09-01", end_date="2022-09-30")
await client.payment_detail("2022-09-15")
# Payment links (price in major units)
link = await client.create_payment_link(name="T-Shirt", price=14.45, min_count=1)
await client.delete_payment_link(link["id"])
# Queries
await client.bin_detail("435508") # card brand / bank / 3D eligibility
await client.installment_rates("req-123") # your commission rates
# Stored cards + recurring (store must have Non3D enabled)
cards = await client.list_cards(utoken) # utoken comes back in the callback
await client.direct_payment(
merchant_oid="ORDER124", email="buyer@example.com",
payment_amount="34.56", # NOTE: major-unit string, unlike the iFrame
user_ip="1.2.3.4", user_name="Jane", user_address="Somewhere 1", user_phone="0555...",
user_basket=[("Item 1", "34.56", 1)],
merchant_ok_url="https://shop.example.com/ok",
merchant_fail_url="https://shop.example.com/fail",
recurring=True, utoken=utoken, ctoken=cards["cards"][0]["ctoken"],
)
Amount gotcha:
create_iframe_token/ linkpricetake minor-unit integers (the library ×100s major units for you), butrefundanddirect_paymenttake a major-unit string like"34.56". Pick the right method — they're signed differently.
Verifying a callback manually:
ok = client.verify_callback(
merchant_oid=oid, status=status, total_amount=total_amount, hash=received_hash
) # always verify before trusting the data; then reply with plain text "OK"
Errors
Non-success API responses raise PayTRAPIError (.message, .code, .scope,
.payload); transport/decoding issues raise PayTRNetworkError; bad config
raises PayTRConfigError. All inherit from PayTRError. Resolve a raw code:
from paytr import describe
describe("payment", "10") # -> "3D Secure required for this transaction"
describe("refund", "009") # -> "Refund exceeds the remaining transaction amount"
Logging
The library logs through the dedicated paytr logger, configured automatically
on import — no setup call needed. It attaches one handler to the paytr logger
only (never the root logger) with propagation off, so it won't interfere with or
double-print through your app's logging.
import os
os.environ["PAYTR_LOG"] = "off" # don't auto-configure; you own the logger
os.environ["PAYTR_LOG"] = "debug" # or set the level (debug/info/warning/...)
# or at runtime:
from paytr import setup_logging
setup_logging("DEBUG", use_colors=False) # idempotent; force=True to replace
To route PayTR logs through your own handlers, set PAYTR_LOG=off and configure
logging.getLogger("paytr") however you like.
HTTP session & lifecycle
Bring your own session for full control of timeouts, connection limits, proxies or retries — you'll never hit limits we picked for you:
PayTRClient(..., session=my_aiohttp_session) # backend auto-detected
PayTRClient(..., session=my_httpx_async_client) # backend auto-detected
With no session, a default aiohttp session is created lazily:
PayTRClient(...) # aiohttp (default), 30s timeout
PayTRClient(..., timeout=None) # no timeout imposed by us
PayTRClient(..., timeout=10) # 10s timeout on the default session
To use httpx, just pass an httpx.AsyncClient as session= (install the
[httpx] extra).
Reuse one instance (it pools connections) and close it on shutdown
(await client.aclose()), or use it as an async context manager. A session you
pass in via session= is yours to manage — we never close it.
Demo app
The src/ tree is a runnable example app (src layout; the library lives under
src/modules/paytr and imports as paytr):
src/
main.py # loads .env, CORS, auto-discovers api/ routers
modules/paytr/ # the library (credential-free, imports as `paytr`)
api/_client.py # configured PayTRClient singleton (env credentials)
api/payment.py # mounts the library router (create_paytr_router)
api/page.py # serves the test page at /paytr/
web/index.html # standalone test page (served, or opened as a file)
uv sync --all-extras
cp src/example.env .env # fill in real credentials
cd src && uv run main.py # http://127.0.0.1:8000/paytr/
uv run pytest # tests (no network)
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 paytr_python-0.1.3.tar.gz.
File metadata
- Download URL: paytr_python-0.1.3.tar.gz
- Upload date:
- Size: 33.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01bc5b18be9508c671f004c5b43b934e8a8672b9e829d277675f64093f7bcda4
|
|
| MD5 |
5b3f827ad16dbd94c04852ad716f3547
|
|
| BLAKE2b-256 |
23725004462e5fefaccdd6fac284ad6d03ba992ab7df27c74511c771c12d77fb
|
Provenance
The following attestation bundles were made for paytr_python-0.1.3.tar.gz:
Publisher:
pypi.yml on HamzaYslmn/PayTR-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paytr_python-0.1.3.tar.gz -
Subject digest:
01bc5b18be9508c671f004c5b43b934e8a8672b9e829d277675f64093f7bcda4 - Sigstore transparency entry: 1735139354
- Sigstore integration time:
-
Permalink:
HamzaYslmn/PayTR-python@b56c0c06db1a40479753a72318e3c784f216f1e1 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/HamzaYslmn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@b56c0c06db1a40479753a72318e3c784f216f1e1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file paytr_python-0.1.3-py3-none-any.whl.
File metadata
- Download URL: paytr_python-0.1.3-py3-none-any.whl
- Upload date:
- Size: 31.6 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 |
9f40552596437a96cfa81b1338b8b86dda8d9cd96ec850d3b384adf6b827fc95
|
|
| MD5 |
962540df54b18768dc5396bda9995d3b
|
|
| BLAKE2b-256 |
b530b4fc9dcb8620a2aff3cd71d9c4d495b3729b1763209d11280759d809756c
|
Provenance
The following attestation bundles were made for paytr_python-0.1.3-py3-none-any.whl:
Publisher:
pypi.yml on HamzaYslmn/PayTR-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paytr_python-0.1.3-py3-none-any.whl -
Subject digest:
9f40552596437a96cfa81b1338b8b86dda8d9cd96ec850d3b384adf6b827fc95 - Sigstore transparency entry: 1735139430
- Sigstore integration time:
-
Permalink:
HamzaYslmn/PayTR-python@b56c0c06db1a40479753a72318e3c784f216f1e1 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/HamzaYslmn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@b56c0c06db1a40479753a72318e3c784f216f1e1 -
Trigger Event:
push
-
Statement type: