Skip to main content

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 / link price take minor-unit integers (the library ×100s major units for you), but refund and direct_payment take 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

paytr_python-0.1.3.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

paytr_python-0.1.3-py3-none-any.whl (31.6 kB view details)

Uploaded Python 3

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

Hashes for paytr_python-0.1.3.tar.gz
Algorithm Hash digest
SHA256 01bc5b18be9508c671f004c5b43b934e8a8672b9e829d277675f64093f7bcda4
MD5 5b3f827ad16dbd94c04852ad716f3547
BLAKE2b-256 23725004462e5fefaccdd6fac284ad6d03ba992ab7df27c74511c771c12d77fb

See more details on using hashes here.

Provenance

The following attestation bundles were made for paytr_python-0.1.3.tar.gz:

Publisher: pypi.yml on HamzaYslmn/PayTR-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for paytr_python-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 9f40552596437a96cfa81b1338b8b86dda8d9cd96ec850d3b384adf6b827fc95
MD5 962540df54b18768dc5396bda9995d3b
BLAKE2b-256 b530b4fc9dcb8620a2aff3cd71d9c4d495b3729b1763209d11280759d809756c

See more details on using hashes here.

Provenance

The following attestation bundles were made for paytr_python-0.1.3-py3-none-any.whl:

Publisher: pypi.yml on HamzaYslmn/PayTR-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page