Python client library for the Garanti BBVA Virtual POS (GVP) payment gateway API with card tokenization and recurring payment support.
Project description
garantipay
A modern, fully-typed Python client library for the Garanti BBVA Virtual POS (GVP) payment gateway API.
Process credit card payments, refunds, and voids through Garanti BBVA's virtual POS system with a clean, Pythonic API.
Features
- One-time payments -- Sale, refund, and void via the VPServlet XML API
- Card tokenization -- Securely store cards on Garanti servers via the Switch JSON API
- Recurring/subscription payments -- Charge stored tokens without asking for card details
- Token management -- Generate, query, and delete stored card tokens
- 3D Secure support -- Process transactions authenticated via 3D Secure
- Fully typed -- Complete type annotations and PEP 561
py.typedmarker - Comprehensive error handling -- Typed exception hierarchy for all failure modes
- Zero magic -- Explicit, readable code with no hidden behavior
Installation
pip install garantipay
Or install from source:
git clone https://github.com/hbtechsoftware/garantipay.git
cd garantipay
pip install .
For development:
pip install -e ".[dev]"
Quick Start
High-Level API (Recommended)
The GarantiClient class handles hash computation, request assembly, and provides convenient methods for common transaction types.
Sale (Payment)
from garantipay import GarantiClient
client = GarantiClient(
terminal_id="12345678", # Terminal No (from Garanti BBVA)
merchant_id="1234567", # Isyeri No (from Garanti BBVA)
prov_user_id="1234567", # Provisioning user ID
user_id="99999999999", # API user ID
password="your_password", # PROVAUT user password
mode="TEST", # "TEST" for sandbox, "PROD" for production
)
response = client.sale(
card_number="1234567890123456",
card_expire_date="0326", # MMYY format
card_cvv2="123",
amount="100", # 100 = 1.00 TRY (last 2 digits are kuruş)
customer_ip="1.2.3.4",
currency="TRY", # optional, defaults to "TRY"
installment_count="", # empty for single payment
)
if response.is_successful:
print(f"Payment approved! Ref: {response.transaction.retref_num}")
else:
print(f"Declined: {response.error_message}")
Refund
response = client.refund(
order_id="ORIGINAL_ORDER_ID", # order ID from the original sale
amount="100", # refund amount (must not exceed original)
customer_ip="1.2.3.4",
currency="TRY",
prov_user_id="PROVRFN", # typically "PROVRFN" for refunds
)
if response.is_successful:
print("Refund approved!")
Void (Cancel)
response = client.void(
order_id="ORIGINAL_ORDER_ID",
amount="100",
customer_ip="1.2.3.4",
retref_num="original_retref_num", # from the sale response
auth_code="original_auth_code", # from the sale response
)
if response.is_successful:
print("Transaction voided!")
3D Secure Sale
from garantipay import GarantiClient, Secure3D
client = GarantiClient(
terminal_id="12345678",
merchant_id="1234567",
prov_user_id="PROVAUT",
user_id="99999999999",
password="your_password",
mode="TEST",
)
# Data received from the 3D Secure callback
secure_3d = Secure3D(
md="merchant_data_from_callback",
txn_id="transaction_id_from_callback",
security_level="3D",
authentication_code="auth_code_from_callback",
)
response = client.sale(
card_number="1234567890123456",
card_expire_date="0326",
card_cvv2="123",
amount="100",
customer_ip="1.2.3.4",
secure_3d=secure_3d,
cardholder_present_code="13",
)
Recurring / Subscription Payments (Switch API)
The Switch API uses card tokenization to enable recurring payments. Store the card once, then charge monthly using the token -- no need to ask the customer for card details again.
Note: The Switch API is a separate system from the VPServlet API. It uses JSON (not XML), SHA-256 (not SHA-1), and requires different credentials (Switch ID + Switch Password).
Step 1: Store the Card (Generate Token)
from garantipay import SwitchClient
switch = SwitchClient(
swt_id="YOUR_SWITCH_ID", # from Garanti BBVA
swt_password="YOUR_SWITCH_PASS", # from merchant management screens
user_id="my_system",
mode="TEST", # "TEST" or "PROD"
)
response = switch.generate_token(
card_number="5549601634451019",
expire_month="02",
expire_year="25",
)
if response.is_successful:
token = response.card.token # Save this in your database!
print(f"Token: {token}")
print(f"Card: {response.card.masked_number}")
Step 2: Charge Monthly (Sale with Token)
payment = switch.sale_with_token(
token="SAVED_TOKEN_FROM_STEP_1",
amount="29.90", # decimal format: 29.90 TRY
customer_ip="192.168.1.1",
currency_code="949", # TRY
)
if payment.is_successful:
print(f"Payment approved! Ref: {payment.acquirer_response.ret_ref_num}")
else:
print(f"Failed: {payment.error_message}")
Step 3: Cancel Subscription (Delete Token)
result = switch.delete_token("SAVED_TOKEN")
if result.is_successful:
print("Card removed from Garanti servers.")
Query a Stored Token
inquiry = switch.inquiry_token(original_request_id="REQUEST_ID_FROM_GENERATE")
if inquiry.is_successful:
print(f"Token: {inquiry.card.token}")
print(f"Bank: {inquiry.card.bank_name}")
Switch API Amount Format
Unlike the VPServlet API, the Switch API uses decimal notation with a period separator:
| Amount String | Actual Value |
|---|---|
"1.00" |
1.00 TRY |
"10.50" |
10.50 TRY |
"29.90" |
29.90 TRY |
"999.99" |
999.99 TRY |
Switch API Test Credentials
| Field | Test Value |
|---|---|
| Switch ID | CC82C381E078482AB328943FCCB7100C |
| Switch Password | 123asdASD@ |
For production credentials, contact ETicaretDestek@garantibbva.com.tr with your Garanti Sanalpos terminal number.
Low-Level API
For advanced use cases where you need full control over every XML field:
from garantipay import (
API, Request, Terminal, Card, Customer,
Transaction, Order, CURRENCIES, sha1,
)
api = API()
request = Request()
request.mode = "TEST"
request.version = "v1.0"
request.terminal = Terminal()
request.terminal.id = "12345678"
request.terminal.merchant_id = "1234567"
request.terminal.user_id = "99999999999"
request.terminal.prov_user_id = "PROVAUT"
request.card = Card()
request.card.number = "1234567890123456"
request.card.expire_date = "0326"
request.card.cvv2 = "123"
request.customer = Customer()
request.customer.ip_address = "1.2.3.4"
request.transaction = Transaction()
request.transaction.amount = "100"
request.transaction.currency_code = CURRENCIES["TRY"]
request.transaction.type = "sales"
request.order = Order()
request.order.order_id = ""
# Compute the required hash
password = "your_password"
hash_password = sha1(password + f"{int(request.terminal.id):09d}").upper()
hash_data = (
f"{request.order.order_id or ''}"
f"{request.terminal.id or ''}"
f"{request.card.number or ''}"
f"{request.transaction.amount or ''}"
f"{hash_password}"
)
request.terminal.hash_data = sha1(hash_data).upper()
response = api.transaction(request)
Response Handling
The Response object provides convenient properties for checking results:
response = client.sale(...)
# Check success
response.is_successful # True if response code is "00"
# Access details
response.response_code # e.g. "00" for success
response.response_message # e.g. "Approved"
response.error_message # Error description if failed
# Transaction details (when successful)
response.transaction.retref_num # Retrieval reference number
response.transaction.auth_code # Authorization code
response.transaction.card_number_masked # e.g. "12345678****3456"
Pretty-Print XML Response
from garantipay import response_to_pretty_xml
pretty = response_to_pretty_xml(response)
print(pretty)
Error Handling
All exceptions inherit from GarantiPayError for easy catch-all handling:
from garantipay.exceptions import (
GarantiPayError,
GarantiPayConnectionError,
GarantiPayValidationError,
GarantiPayXMLError,
)
try:
response = client.sale(...)
except GarantiPayValidationError as exc:
# Invalid input parameters (caught before any network call)
print(f"Validation error: {exc}")
except GarantiPayConnectionError as exc:
# Network/timeout error communicating with the API
print(f"Connection error: {exc}")
except GarantiPayXMLError as exc:
# Unexpected response format from the API
print(f"XML parsing error: {exc}")
except GarantiPayError as exc:
# Catch-all for any library error
print(f"Error: {exc}")
Supported Currencies
| Code | Currency | ISO 4217 |
|---|---|---|
TRY |
Turkish Lira | 949 |
USD |
US Dollar | 840 |
EUR |
Euro | 978 |
GBP |
British Pound | 826 |
JPY |
Japanese Yen | 392 |
Aliases TL, YTL, and TRL also map to Turkish Lira (949).
Amount Format
The API expects amounts as strings where the last 2 digits represent the fractional part (kuruş/cents):
| Amount String | Actual Value |
|---|---|
"100" |
1.00 TRY |
"1000" |
10.00 TRY |
"1050" |
10.50 TRY |
"99999" |
999.99 TRY |
Logging
The library uses Python's standard logging module under the "garantipay" logger name. Enable debug logging to see raw XML requests and responses:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("garantipay")
logger.setLevel(logging.DEBUG)
Warning: Debug logs may contain sensitive data (card numbers, passwords). Never enable debug logging in production.
Project Structure
garantipay/
├── pyproject.toml # Package metadata and build config
├── README.md # This file
├── LICENSE # MIT license
├── CHANGELOG.md # Version history
├── src/
│ └── garantipay/
│ ├── __init__.py # Public API exports
│ ├── py.typed # PEP 561 typed package marker
│ ├── client.py # API and GarantiClient (VPServlet)
│ ├── constants.py # VPServlet endpoints and currencies
│ ├── exceptions.py # Exception hierarchy
│ ├── hash.py # SHA-1 hashing (VPServlet)
│ ├── models.py # VPServlet request/response models
│ ├── xml_builder.py # Request XML serializer
│ ├── xml_parser.py # Response XML parser
│ ├── switch_client.py # SwitchClient (tokenization & payments)
│ ├── switch_constants.py # Switch API endpoints
│ ├── switch_hash.py # SHA-256 hashing (Switch)
│ └── switch_models.py # Switch JSON request/response models
└── examples/
├── sale.py # Sale transaction example
├── refund.py # Refund transaction example
├── sale_3d_secure.py # 3D Secure sale example
├── low_level_api.py # Low-level API example
├── token_generate.py # Card tokenization example
└── token_recurring_payment.py # Full recurring payment flow
Requirements
- Python 3.9+
requests>= 2.28.0
License
This project is licensed under the MIT License. See LICENSE for details.
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 garantipay-1.1.0.tar.gz.
File metadata
- Download URL: garantipay-1.1.0.tar.gz
- Upload date:
- Size: 27.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1d4483ac33ed90dc8d21bf132f6574227b8bdc080b1050bbe0c09088fd0d163
|
|
| MD5 |
7ce364787f5956f2b0d572ceded55fa9
|
|
| BLAKE2b-256 |
12e36e22d145d92d5ce95fddba3859ba64b6fc6c52e04c521a1b10d28457e948
|
File details
Details for the file garantipay-1.1.0-py3-none-any.whl.
File metadata
- Download URL: garantipay-1.1.0-py3-none-any.whl
- Upload date:
- Size: 34.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f836f36735498a0eac570e4b3e115faac8da99673e1a21af39f7e9a1b4bf888f
|
|
| MD5 |
941c612855cb980ff405ab2ac7a7112e
|
|
| BLAKE2b-256 |
d49ee62d406b924a93ce3055b508f2617bd868ee3dfafac18df5374b850f50a2
|