Skip to main content

django-payments-tr

Payment processing for Django with Turkey-specific features.

PyPI version Python versions Django versions License: MIT

Features

  • Provider Abstraction: Unified interface for multiple payment gateways (iyzico, Stripe)
  • Turkey-Specific Utilities:
    • KDV (VAT) calculation with current Turkish rates
    • TC Kimlik No validation
    • Turkish IBAN validation
    • VKN (Tax Number) validation
    • Turkish phone number validation
  • EFT Payment Workflow: Complete EFT payment handling with admin approval
  • Django Integration: Admin mixins, model mixins, DRF serializers

Installation

# Basic installation
pip install django-payments-tr

# With iyzico support
pip install django-payments-tr[iyzico]

# With Stripe support
pip install django-payments-tr[stripe]

# With both providers
pip install django-payments-tr[all]

Quick Start

1. Add to INSTALLED_APPS

INSTALLED_APPS = [
    # ...
    "payments_tr",
]

2. Configure Payment Provider

# settings.py
PAYMENT_PROVIDER = "iyzico"  # or "stripe"

# For iyzico
IYZICO_API_KEY = "your-api-key"
IYZICO_SECRET_KEY = "your-secret-key"
IYZICO_BASE_URL = "api.iyzipay.com"  # or sandbox-api.iyzipay.com

# For Stripe
STRIPE_SECRET_KEY = "sk_live_..."
STRIPE_WEBHOOK_SECRET = "whsec_..."

3. Use the Provider

from payments_tr import get_payment_provider

# Get configured provider
provider = get_payment_provider()

# Create a payment
result = provider.create_payment(
    payment,
    callback_url="https://example.com/callback",
    buyer_info={
        "email": "customer@example.com",
        "name": "John",
        "surname": "Doe",
    }
)

if result.success:
    # For iyzico: redirect to checkout
    redirect(result.checkout_url)
    # For Stripe: send client_secret to frontend
    return {"client_secret": result.client_secret}

Turkey-Specific Utilities

KDV (VAT) Calculation

from payments_tr.tax import KDVRate, calculate_kdv, amount_with_kdv, extract_kdv

# Calculate KDV
kdv = calculate_kdv(10000)  # 2000 (20% of 100 TRY)
kdv = calculate_kdv(10000, KDVRate.REDUCED)  # 1000 (10%)

# Calculate gross amount
gross = amount_with_kdv(10000)  # 12000 (100 TRY + 20% KDV)

# Extract net and KDV from gross
net, kdv = extract_kdv(12000)  # (10000, 2000)

Validation

from payments_tr.validation import (
    validate_tckn,
    validate_iban_tr,
    validate_vkn,
    validate_phone_tr,
    format_phone,
)

# TC Kimlik No
if validate_tckn("10000000146"):
    print("Valid TC Kimlik No")

# Turkish IBAN
if validate_iban_tr("TR330006100519786457841326"):
    print("Valid Turkish IBAN")

# Phone formatting
formatted = format_phone("5551234567")  # "+90 555 123 45 67"

EFT Payment Workflow

Add EFT Fields to Your Model

from django.db import models
from payments_tr.eft import EFTPaymentFieldsMixin

class Payment(EFTPaymentFieldsMixin, models.Model):
    amount = models.IntegerField()
    # ... your other fields

    def confirm(self):
        # Your confirmation logic
        pass

Admin Integration

from django.contrib import admin
from payments_tr.eft import EFTPaymentAdminMixin

@admin.register(Payment)
class PaymentAdmin(EFTPaymentAdminMixin, admin.ModelAdmin):
    list_display = ['id', 'amount', 'eft_status_display']
    # Adds approve/reject actions automatically

EFT Approval Service

from payments_tr.eft import EFTApprovalService

service = EFTApprovalService()

# Approve a payment
result = service.approve_payment(payment, admin_user)

# Reject with reason
result = service.reject_payment(payment, admin_user, reason="Invalid receipt")

Provider Abstraction

Using Multiple Providers

from payments_tr import get_payment_provider

# Get default (from settings)
provider = get_payment_provider()

# Get specific provider by name
iyzico = get_payment_provider("iyzico")
stripe = get_payment_provider("stripe")

Per-Country Provider Selection

For multi-country deployments, configure different providers per country:

# settings.py
PAYMENT_PROVIDERS_BY_COUNTRY = {
    "TR": "iyzico",   # Turkey uses iyzico
    "US": "stripe",   # USA uses Stripe
    "GB": "stripe",   # UK uses Stripe
    "BR": "pagarme",  # Brazil uses local provider (when implemented)
}
PAYMENT_PROVIDER = "stripe"  # Default fallback for unconfigured countries
from payments_tr import get_payment_provider

# Get provider based on user's country
provider = get_payment_provider(country_code="TR")  # Returns iyzico
provider = get_payment_provider(country_code="US")  # Returns Stripe
provider = get_payment_provider(country_code="XX")  # Falls back to default (Stripe)

# Explicit provider name overrides country
provider = get_payment_provider(name="stripe", country_code="TR")  # Returns Stripe

Helper Functions

from payments_tr import (
    get_provider_for_country,
    get_provider_name,
    get_supported_countries,
    get_available_providers,
    is_iyzico_enabled,
    is_stripe_enabled,
)

# Get provider name for a country
get_provider_for_country("TR")  # "iyzico"
get_provider_for_country("XX")  # "stripe" (default)

# Get provider name (with optional country)
get_provider_name()             # "stripe" (default)
get_provider_name("TR")         # "iyzico"

# Get all configured country mappings
get_supported_countries()       # {"TR": "iyzico", "US": "stripe", ...}

# Get all registered providers
get_available_providers()       # ["stripe", "iyzico"]

# Check which provider is active
is_iyzico_enabled()             # False (checking default)
is_iyzico_enabled("TR")         # True
is_stripe_enabled("US")         # True

Creating Custom Providers

from payments_tr import PaymentProvider, PaymentResult, register_provider

class PayTRProvider(PaymentProvider):
    provider_name = "paytr"

    def create_payment(self, payment, **kwargs):
        # Your implementation
        return PaymentResult(success=True, ...)

    def confirm_payment(self, provider_payment_id):
        return PaymentResult(success=True, ...)

    def create_refund(self, payment, amount=None, reason="", **kwargs):
        return RefundResult(success=True, ...)

    def handle_webhook(self, payload, signature=None, **kwargs):
        return WebhookResult(success=True, ...)

    def get_payment_status(self, provider_payment_id):
        return "succeeded"

# Register your provider
register_provider("paytr", PayTRProvider)

Marketplace (sub-merchant) payments

For marketplace apps that need to split each basket item between the platform and a seller, django-payments-tr v0.5.0+ ships first-class support for Iyzico's marketplace flow ("işyeri gelir paylaşımı"). See docs/marketplace.md for the full walkthrough.

Register a sub-merchant once

from payments_tr.providers.iyzico import SubMerchantClient, SubMerchantType

client = SubMerchantClient()
resp = client.create(
    external_id="station-42",
    sub_merchant_type=SubMerchantType.LIMITED_OR_JOINT_STOCK_COMPANY,
    legal_company_title="Acme Print AŞ",
    contact_name="Aslı",
    contact_surname="Yılmaz",
    email="acme@example.com",
    gsm_number="+905551234567",
    iban="TR330006100519786457841326",   # validated via validate_iban_tr
    tax_office="Beşiktaş",
    tax_number="1234567890",             # validated via validate_vkn
)
seller.iyzico_sub_merchant_key = resp.sub_merchant_key

The AbstractSubMerchantOwner model mixin provides ready-made storage fields and a clean() validator:

from payments_tr.providers.iyzico.models import AbstractSubMerchantOwner

class PrintStation(AbstractSubMerchantOwner):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    name = models.CharField(max_length=200)

Create a marketplace payment

A multi-file order routed to a single seller (typical print-shop flow):

basket_items = [
    {
        "id": str(f.id),
        "name": f.original_filename[:100],
        "category1": "Baskı Hizmeti",
        "itemType": "PHYSICAL",
        "price": str(f.subtotal),
        "subMerchantKey": order.station.iyzico_sub_merchant_key,
        "subMerchantPrice": str(f.station_earnings),  # platform commission deducted
    }
    for f in order.files.all()
]

result = provider.create_payment(
    payment,
    callback_url="https://example.com/callback/",
    buyer_info={"email": "customer@example.com", ...},
    basket_items=basket_items,
    marketplace=True,            # strict: every item must be sub-merchant routed
)

Mixed baskets (some marketplace, some platform-only) are accepted when you omit marketplace=True — Iyzico keeps full revenue on the unrouted items.

Refund a single item's seller share

provider.create_refund(
    payment,
    amount=item.sub_merchant_price * 100,            # kuruş
    payment_transaction_id=item.payment_transaction_id,
    ip_address=request.META["REMOTE_ADDR"],
)

Without payment_transaction_id the refund targets the order-level payment id — same behaviour as non-marketplace refunds.

DRF Serializers

from payments_tr.contrib.serializers import (
    PaymentIntentCreateSerializer,
    PaymentResultSerializer,
    EFTPaymentCreateSerializer,
)

class PaymentViewSet(viewsets.ModelViewSet):
    @action(detail=True, methods=['post'])
    def create_intent(self, request, pk=None):
        serializer = PaymentIntentCreateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        provider = get_payment_provider()
        result = provider.create_payment(
            self.get_object(),
            **serializer.validated_data
        )

        return Response(PaymentResultSerializer(result.to_dict()).data)

Architecture

This package provides a complete payment solution with embedded provider clients:

┌───────────────────────────────────────────────────────────────────┐
│                      django-payments-tr                           │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │  Provider Abstraction Layer                                  │  │
│  │  - PaymentProvider abstract base class                       │  │
│  │  - PaymentResult, RefundResult, WebhookResult               │  │
│  │  - Per-country provider selection                            │  │
│  └─────────────────────────────────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │  Turkey-Specific Features                                    │  │
│  │  - KDV (VAT) calculation                                     │  │
│  │  - TCKN, IBAN, VKN validation                               │  │
│  │  - EFT payment workflow                                      │  │
│  └─────────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────┐    ┌────────────────────────┐        │
│  │  IyzicoClient          │    │  StripeProvider        │        │
│  │  (embedded)            │    │                        │        │
│  │  - Checkout forms      │    │  - PaymentIntent       │        │
│  │  - 3D Secure           │    │  - Webhooks            │        │
│  │  - Installments        │    │  - Refunds             │        │
│  │  - Subscriptions       │    │  - Subscriptions       │        │
│  │  - Card storage        │    │                        │        │
│  └───────────┬────────────┘    └───────────┬────────────┘        │
└──────────────┼─────────────────────────────┼─────────────────────┘
               ▼                             ▼
      ┌─────────────────┐           ┌─────────────────┐
      │    iyzipay      │           │     stripe      │
      │  (Python SDK)   │           │  (Python SDK)   │
      └─────────────────┘           └─────────────────┘

Note: The iyzico client is fully embedded in this package. You don't need a separate django-iyzico installation.

Low-Level iyzico Client

For advanced use cases, you can access the embedded IyzicoClient directly:

from payments_tr.providers.iyzico import get_client, IyzicoClient

# Get a configured client (uses Django settings)
client = get_client()

# Or create with explicit settings
client = IyzicoClient(
    api_key="your-api-key",
    secret_key="your-secret-key",
    base_url="https://sandbox-api.iyzipay.com"
)

# Create checkout form
response = client.create_checkout_form(
    order_data={
        "locale": "tr",
        "conversationId": "123456",
        "price": "100.00",
        "paidPrice": "100.00",
        "currency": "TRY",
        "basketId": "B123",
        "paymentGroup": "PRODUCT",
    },
    buyer={
        "id": "BY789",
        "name": "John",
        "surname": "Doe",
        "email": "john@example.com",
        "gsmNumber": "+905551234567",
        "identityNumber": "11111111111",
        "registrationAddress": "Istanbul, Turkey",
        "city": "Istanbul",
        "country": "Turkey",
        "ip": "85.34.78.112",
    },
    billing_address={
        "contactName": "John Doe",
        "city": "Istanbul",
        "country": "Turkey",
        "address": "Istanbul, Turkey",
    },
    basket_items=[
        {
            "id": "ITEM1",
            "name": "Product Name",
            "category1": "Category",
            "itemType": "PHYSICAL",
            "price": "100.00",
        }
    ],
    callback_url="https://example.com/callback",
)

# Retrieve checkout form result (after callback)
result = client.retrieve_checkout_form(token)

# Process refund
refund_response = client.refund_payment(
    payment_id="12345678",
    ip_address="85.34.78.112",
    amount=Decimal("50.00"),  # Partial refund
    reason="Customer request",
)

iyzico Client Features

The embedded client supports:

Feature Method
Checkout Form create_checkout_form(), retrieve_checkout_form()
3D Secure Automatic with checkout forms
Refunds refund_payment() (full and partial)
Installments get_installment_info()
Card Storage create_card(), delete_card()
BIN Lookup retrieve_bin_number()
Subscriptions payments_tr.providers.iyzico.subscriptions module

iyzico Model Mixin

For Django models with iyzico-specific fields:

from django.db import models
from payments_tr.providers.iyzico.models import AbstractIyzicoPayment

class Payment(AbstractIyzicoPayment):
    """Payment model with iyzico fields pre-configured."""
    user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
    description = models.TextField(blank=True)

    # AbstractIyzicoPayment provides:
    # - iyzico_token
    # - iyzico_conversation_id
    # - iyzico_payment_id
    # - status, amount, currency fields
    # - QuerySet methods for filtering by status

Configuration Reference

# settings.py

# Default payment provider (used when no country match)
PAYMENT_PROVIDER = "stripe"  # or "iyzico"

# Per-country provider mapping (optional)
PAYMENT_PROVIDERS_BY_COUNTRY = {
    "TR": "iyzico",   # Turkey uses iyzico
    "US": "stripe",   # USA uses Stripe
    "GB": "stripe",   # UK uses Stripe
}

# iyzico settings
IYZICO_API_KEY = "..."
IYZICO_SECRET_KEY = "..."
IYZICO_BASE_URL = "api.iyzipay.com"

# Stripe settings
STRIPE_SECRET_KEY = "sk_..."
STRIPE_PUBLISHABLE_KEY = "pk_..."
STRIPE_WEBHOOK_SECRET = "whsec_..."

Data Processing & Sub-processors

This section documents which personal data leaves your application when you use django-payments-tr, where it goes, and under what legal transfer mechanism. Downstream integrators must reflect these flows in their own RoPA (Records of Processing Activities under GDPR Art. 30 / KVKK Art. 16) and Privacy Notices. This is informational — it does not constitute legal advice.

Sub-processor: Iyzico (Türkiye)

Operating entity: Iyzi Ödeme ve Elektronik Para Hizmetleri A.Ş. (BDDK-licensed payment institution, registered in Türkiye).

Data sent on every payment / checkout-form call:

  • Buyer email
  • Buyer first name + last name
  • Buyer phone (gsmNumber) when available
  • Buyer Turkish identity number (TCKN) — mandatory for Turkish cardholder transactions
  • Buyer registration address, city, country
  • Buyer IP address (subject to IYZICO_STRICT_IP_VALIDATION)
  • Order amount, currency, basket items (id, name, category, price)
  • For marketplace flows: sub-merchant key + per-item revenue split

Data sent on sub-merchant onboarding (marketplace mode only):

  • Seller IBAN (validated client-side via validate_iban_tr)
  • Seller TCKN for PERSONAL type, or VKN + tax office + legal company title for PRIVATE_COMPANY / LIMITED_OR_JOINT_STOCK_COMPANY types

Transfer mechanism: Türkiye does not have an EU adequacy decision under GDPR Art. 45. EU-based controllers must rely on Standard Contractual Clauses (Art. 46) plus a Transfer Impact Assessment. Iyzico's data-processing addendum is available on request from Iyzico directly. KVKK-only deployments do not need an SCC because data stays within Türkiye.

Sub-processor: Stripe

Operating entity: Stripe Payments Europe Ltd. (Ireland) for EU customers; Stripe, Inc. (US) for global customers. Routing depends on your Stripe account configuration.

Data sent on every payment:

  • Buyer email
  • Order amount, currency, payment metadata you supply
  • Card data — collected client-side via Stripe.js / Stripe Elements and tokenised before it ever touches your server. The host application never sees the PAN, expiry, or CVV.

Transfer mechanism: Stripe's standard DPA (covers SCCs for EU→US transfers via Stripe, Inc.). Available at https://stripe.com/legal/dpa.

What stays in your database

The library writes to your tables (not Iyzico's / Stripe's) the following payment-related fields. These are under your sole controllership:

  • Provider name, provider transaction id, idempotency key
  • Amount, currency, status (pending / success / failed / refunded)
  • Buyer email and a hash / reference to the buyer user
  • Webhook event records (for replay protection — see WebhookEvent in SECURITY.md)
  • For EFT: bank reference, approval audit trail
  • For marketplace: sub-merchant external id and key (no IBAN / TCKN unless your seller model opts in via AbstractSubMerchantOwner)

We do not store: full card numbers, CVV, magnetic-stripe data, or any data that would put your application in PCI DSS scope. The PaymentMethod model intentionally stores only Iyzico's tokenised references (card_token, card_user_key, last 4 digits, expiry).

Log retention

Payment records contain identifiers and email addresses, which are personal data under GDPR / KVKK. Old records must be purged on a defined schedule. Use the bundled management command:

# Keep successful payments for 7 years (TR commercial-code retention),
# purge failures after 90 days. Schedule via cron / Celery beat.
python manage.py cleanup_old_payments \
    --keep-successful 2555 \
    --keep-failed 90

See SECURITY.md → "Data retention" for the full rationale and jurisdiction-specific values.

Development

# Clone the repo
git clone https://github.com/aladagemre/django-payments-tr
cd django-payments-tr

# Install dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run linting
ruff check .

# Run type checking
mypy src

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please read our Contributing Guide for details.

  • django-iyzico - The iyzico client is now embedded in django-payments-tr. The standalone package is deprecated in favor of this unified solution.

Metadata

Release files for django-payments-tr 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-payments-tr 0.6.0
File Size Uploaded Provenance Actions
django_payments_tr-0.6.0.tar.gz 305.2 kB Without provenance Details for django_payments_tr-0.6.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-payments-tr 0.6.0
File Interpreter ABI Platform Actions
django_payments_tr-0.6.0-py3-none-any.whl Python 3 none any Details for django_payments_tr-0.6.0-py3-none-any.whl

Total release size: 509.6 kB

Release files / django_payments_tr-0.6.0.tar.gz

Download URL django_payments_tr-0.6.0.tar.gz
Size 305.2 kB
Tags Source
SHA-256 checksum
How to use checksums
19d551e10fa9f2e9365ce9aa7134f83321e9ebe4c604157a3433a216e29f64a1
BLAKE2b-256 checksum
How to use checksums
983baca3aaced38f59692c1af52d6534e71150de68a7b00f2e3ac1fa493747fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / django_payments_tr-0.6.0-py3-none-any.whl

Download URL django_payments_tr-0.6.0-py3-none-any.whl
Size 204.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f737bbbcb927ad154746992643c031b4aedeeb0909619eaa6e8b5867f5dc0a6d
BLAKE2b-256 checksum
How to use checksums
a459b66dd95fcbad21e9aecd7b586c5c8ef8666ace9501a01ba2371a2be81a49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Security for django-payments-tr 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page