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).

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 (+ _client.py: configured singleton)
  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)
uv run python test/excardtest.py    # create a test token + iFrame URL

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.1.tar.gz (30.9 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.1-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

File details

Details for the file paytr_python-0.1.1.tar.gz.

File metadata

  • Download URL: paytr_python-0.1.1.tar.gz
  • Upload date:
  • Size: 30.9 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.1.tar.gz
Algorithm Hash digest
SHA256 ed6a5306055c079da7210c97e0cce4fe793e02d80a218ce4982dcb2f03520160
MD5 2ad2ea1a32394f7151e0b6a7ffc7bb8a
BLAKE2b-256 4e0d2377424515c99da712f86d9533f7d093b1d65126facdefcd13450e83dc49

See more details on using hashes here.

Provenance

The following attestation bundles were made for paytr_python-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: paytr_python-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 30.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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 443abaa99d4c5c47e6d650f0a66531341419a9deafd9e0b41839f22246466830
MD5 719fccd168fccb7ccb3903db64fe2450
BLAKE2b-256 02517f72cb9fc4aa31fd72e0823bf2016e11c3d317331bf073f4e1913b8ed236

See more details on using hashes here.

Provenance

The following attestation bundles were made for paytr_python-0.1.1-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