Python SDK for the Azul Payment Gateway. PyAzul allows you to process payments, tokenize cards, and implement 3D Secure authentication by interacting with the Azul API.
pip install pyazul
Create a .env
file in your project root:
# Basic API Credentials
AUTH1=your_auth1
AUTH2=your_auth2
MERCHANT_ID=your_merchant_id
ENVIRONMENT=dev # 'dev' for testing, 'prod' for live
# SSL Certificates (Path, PEM string, or Base64 PEM)
AZUL_CERT=/path/to/certificate.pem
AZUL_KEY=/path/to/key.pem
# Payment Page Settings (Optional)
AZUL_AUTH_KEY=your_auth_key_for_page
MERCHANT_NAME=Your_Business_Name
MERCHANT_TYPE=Your_Business_Type
Note on Certificates: AZUL_CERT
and AZUL_KEY
can be file paths. Alternatively, AZUL_CERT
can be the full PEM content string, and AZUL_KEY
can be the PEM content string or Base64 encoded. The library handles temporary files internally if direct content is provided.
from pyazul import PyAzul
azul = PyAzul() # Loads settings from .env and environment variables
# For custom settings (e.g., if not using .env):
# from pyazul.core.config import AzulSettings
# settings = AzulSettings(AUTH1="...", AUTH2="...", ...)
# azul = PyAzul(settings=settings)
PyAzul uses standard Python logging. To enable detailed debug logs:
import logging
logging.getLogger('pyazul').setLevel(logging.DEBUG)
# For specific services, e.g.: logging.getLogger('pyazul.services.secure').setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG) # Ensure a handler is configured
Ensure azul = PyAzul()
is initialized.
from pyazul import PyAzul, AzulError
# azul = PyAzul() # Assumed initialized
async def process_payment():
payment_data = {
# Channel and PosInputMode default to "EC" and "E-Commerce"
"Amount": 100000, # In cents (e.g., 1000.00)
"Itbis": 18000, # In cents (e.g., 180.00)
"CardNumber": "4111********1111", # Use a test card
"Expiration": "202812", # YYYYMM
"CVC": "***",
"OrderNumber": "INV-12345",
}
try:
response = await azul.sale(payment_data)
if response.get("ResponseMessage") == "APROBADA":
print(f"Payment approved: {response.get('AuthorizationCode')}, AzulOrderId: {response.get('AzulOrderId')}")
else:
print(f"Error: {response.get('ResponseMessage')}, Details: {response.get('ErrorDescription', response)}")
except AzulError as e:
print(f"An API error occurred: {e}")
# from pyazul import PyAzul, AzulError # Assumed imported
# azul = PyAzul() # Assumed initialized
async def tokenize_card():
token_creation_data = {
"CardNumber": "4111********1111",
"Expiration": "202812",
# Store defaults to your Merchant ID from settings
}
try:
token_response = await azul.create_token(token_creation_data)
if token_response.get("ResponseMessage") == "APROBADA":
token_id = token_response.get("DataVaultToken")
print(f"Token created: {token_id}")
# Example: Use token for a non-3DS payment
token_payment_data = {
"DataVaultToken": token_id,
"Amount": 50000, # 500.00 in cents
"Itbis": 0, # 0.00 in cents
"OrderNumber": "TOKEN-ORD-XYZ"
}
payment_response = await azul.token_sale(token_payment_data)
if payment_response.get("ResponseMessage") == "APROBADA":
print(f"Token payment approved: {payment_response.get('AuthorizationCode')}")
else:
print(f"Token payment error: {payment_response.get('ResponseMessage')}")
else:
print(f"Token creation error: {token_response.get('ResponseMessage')}")
except AzulError as e:
print(f"An API error occurred: {e}")
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from pyazul import PyAzul
# app = FastAPI()
# azul = PyAzul() # Assumed initialized
@app.get("/pay/{order_id}", response_class=HTMLResponse)
async def get_payment_page(order_id: str):
page_data = {
"Amount": 100000, # 1000.00 in cents
"ITBIS": 18000, # 180.00 in cents
"OrderNumber": order_id,
"ApprovedUrl": "https://your-site.com/success",
"DeclineUrl": "https://your-site.com/declined",
"CancelUrl": "https://your-site.com/cancel",
# Other fields like UseStoreLogo, ShowLoyaltyBar are optional
}
html_form = azul.payment_page(page_data)
return HTMLResponse(content=html_form)
PyAzul supports 3D Secure 2.0. This involves interactions between the customer's browser, your server, the card issuer's ACS, and Azul. PyAzul simplifies this by initiating the transaction, providing helpers for redirection, processing ACS callbacks, and finalizing the transaction.
A unique secure_id
(UUID string generated by PyAzul) is created for each 3DS transaction. PyAzul appends this secure_id
as a query parameter (e.g., ?secure_id=<generated_id>
) to your provided TermUrl
and MethodNotificationUrl
. Your application must extract this secure_id
from callback query parameters to track the 3DS session.
Application-Side State Management: For production, your application MUST implement its own persistent session management (e.g., Redis, database) to reliably manage state across asynchronous 3DS callbacks. Use the secure_id
to store/retrieve your order details, the azul_order_id
(from the initial secure_sale
response or via await azul.get_session_info(secure_id)
), and the original TermUrl
. PyAzul's internal session store (await azul.get_session_info(secure_id)
) is for its operational needs and is not persistent.
from fastapi import FastAPI, Form, Query
from fastapi.responses import HTMLResponse, JSONResponse
from pyazul import PyAzul, AzulError
app = FastAPI()
azul = PyAzul() # Assumed initialized
# --- Application-Level Session Store (Example - REPLACE FOR PRODUCTION) ---
app_level_session_store = {} # Use Redis, database, etc., in production.
# --- End Example Session Store ---
# 1. Transaction Initiation
@app.post("/initiate-3ds-payment")
async def initiate_3ds_payment(request_data: dict): # Your payment request data
original_term_url = request_data.get("threeDSAuth", {}).get("TermUrl")
try:
response = await azul.secure_sale(request_data)
secure_id = response.get("id") # PyAzul's unique ID for this 3DS session
azul_order_id_initial = response.get("value", {}).get("AzulOrderId")
if secure_id:
# Store in YOUR application's session store
app_level_session_store[secure_id] = {
"original_term_url": original_term_url,
"azul_order_id": azul_order_id_initial,
"internal_order_ref": request_data.get("OrderNumber"),
}
# If azul_order_id wasn't in the immediate response (e.g., redirect occurred),
# try getting it from PyAzul's internal session for your app's store
if not azul_order_id_initial:
pyazul_session = await azul.get_session_info(secure_id)
if pyazul_session and pyazul_session.get("azul_order_id"):
app_level_session_store[secure_id]["azul_order_id"] = pyazul_session.get("azul_order_id")
if response.get("redirect"): # HTML for redirection to 3DS Method or ACS Challenge
return HTMLResponse(content=response.get("html"))
elif response.get("value", {}).get("ResponseMessage") == "APROBADA": # Frictionless approval
return JSONResponse(content={"status": "approved_frictionless", "data": response.get("value")})
else: # Other initial responses (e.g., declined)
return JSONResponse(content={"status": "error_or_declined_early", "data": response.get("value")}, status_code=400)
except AzulError as e:
return JSONResponse(content={"status": "sdk_error", "message": str(e)}, status_code=500)
# 2. 3DS Method Notification Callback (Your MethodNotificationUrl)
@app.post("/capture-3ds-method") # Matches MethodNotificationUrl
async def capture_3ds_method_callback(secure_id: str = Query(...)): # secure_id from query
try:
app_session = app_level_session_store.get(secure_id)
if not app_session or not app_session.get("azul_order_id") or not app_session.get("original_term_url"):
# Attempt to fetch from PyAzul's session as a fallback if critical info missing
pyazul_session_fallback = await azul.get_session_info(secure_id)
if not app_session: app_session = {} # Initialize if was None
if pyazul_session_fallback:
if not app_session.get("azul_order_id"):
app_session["azul_order_id"] = pyazul_session_fallback.get("azul_order_id")
if not app_session.get("original_term_url"): # PyAzul stores its version with ?secure_id
# We need the user's original TermUrl for create_challenge_form if it wasn't stored
# This part of the example assumes original_term_url was robustly stored by the app.
# For simplicity, we'll proceed assuming it was.
# In a real app, handle this more gracefully or ensure it's always in app_session.
pass
if not app_session.get("azul_order_id") or not app_session.get("original_term_url"):
return JSONResponse(content={"error": "Critical session data (AzulOrderId or original TermUrl) missing"}, status_code=400)
result = await azul.process_3ds_method(
azul_order_id=app_session["azul_order_id"],
method_notification_status="RECEIVED"
)
if result.get("ResponseMessage") == "3D_SECURE_CHALLENGE":
# Use the TermUrl from your app session. PyAzul's internal one already has secure_id.
8000
# If you stored the user's original TermUrl, append secure_id yourself.
term_url_for_challenge = app_session["original_term_url"]
if "?secure_id=" not in term_url_for_challenge: # Ensure secure_id is present
term_url_for_challenge += f"?secure_id={secure_id}"
form_html = azul.create_challenge_form(
result["ThreeDSChallenge"]["CReq"],
term_url_for_challenge,
result["ThreeDSChallenge"]["RedirectPostUrl"]
)
return HTMLResponse(content=form_html)
elif result.get("ResponseMessage") == "APROBADA":
return JSONResponse(content={"status": "approved_after_method", "data": result})
else:
return JSONResponse(content={"status": "pending_or_error_after_method", "data": result})
except AzulError as e:
return JSONResponse(content={"status": "sdk_error", "message": str(e)}, status_code=500)
# 3. 3DS Challenge Completion Callback (Your TermUrl)
@app.post("/post-3ds-callback") # Matches TermUrl
async def post_3ds_challenge_callback(secure_id: str = Query(...), CRes: str = Form(...)):
try:
final_result = await azul.process_challenge(session_id=secure_id, challenge_response=CRes)
if final_result.get("ResponseMessage") == "APROBADA":
return JSONResponse(content={"status": "approved_after_challenge", "data": final_result})
else:
return JSONResponse(content={"status": "declined_or_error_after_challenge", "data": final_result}, status_code=400)
except AzulError as e:
return JSONResponse(content={"status": "sdk_error", "message": str(e)}, status_code=500)
- Callback URLs: Ensure
TermUrl
andMethodNotificationUrl
correctly point to your server endpoints. CardHolderInfo
andThreeDSAuth
: Provide accurate data in these objects duringsecure_sale
initiation.- Error Handling: Implement robust error handling for each step.
PyAzul methods accept Python dictionaries for request data. The SDK's internal Pydantic models handle formatting for fields like Amount
and ITBIS
(provide as integers representing cents).
Example: secure_sale
Request Data
{
"CardNumber": "4111...", "Expiration": "202812", "CVC": "123",
"Amount": 100000, # Integer cents (e.g., 1000.00)
"Itbis": 18000, # Integer cents (e.g., 180.00)
"OrderNumber": "3DS-ORDER-123",
# Store defaults to MerchantID from settings
# forceNo3DS defaults to "0" (enable 3DS)
"cardHolderInfo": { # Provide as much detail as possible
"Name": "John Doe", "Email": "john@example.com",
"BillingAddressCity": "Santo Domingo", "BillingAddressCountry": "DO",
# ... other cardHolderInfo fields
},
"threeDSAuth": { # Required for secure transactions
"TermUrl": "https://your-domain.com/post-3ds-callback",
"MethodNotificationUrl": "https://your-domain.com/capture-3ds-method",
# RequestChallengeIndicator defaults to "03" (Prefer challenge)
# ... other threeDSAuth fields
}
}
Refer to pyazul/models/
for Pydantic model definitions if you wish to use them for type hinting or constructing request data, then pass your_model_instance.model_dump(exclude_none=True)
.
The PyAzul
object is the main entry point. Key methods:
# azul = PyAzul() # Assumed initialized
# --- Non-3DS Transactions ---
# await azul.sale(data_dict)
# await azul.token_sale(data_dict)
# await azul.hold(data_dict)
# await azul.post_auth(data_dict) # Capture a hold
# await azul.void(data_dict)
# await azul.refund(data_dict)
# await azul.verify_transaction(data_dict) # Verify transaction status
# --- Card Tokenization (DataVault) ---
# await azul.create_token(data_dict)
# await azul.delete_token(data_dict)
# --- Payment Page ---
# html_form_string = azul.payment_page(data_dict)
# --- 3D Secure ---
# response = await azul.secure_sale(data_dict)
# response = await azul.secure_token_sale(data_dict)
# response = await azul.secure_hold(data_dict)
# --- 3DS Callback Helpers ---
# response = await azul.process_3ds_method(azul_order_id="...", method_notification_status="...")
# response = await azul.process_challenge(session_id="...", challenge_response="...")
# html_form = azul.create_challenge_form(creq="...", term_url="...", redirect_post_url="...")
# --- 3DS Session Inspection (Primarily for PyAzul's internal state) ---
# session_data = await azul.get_session_info(session_id="...")
# Access to underlying components (generally not needed for typical use):
# azul.transaction, azul.datavault, azul.payment_page_service, azul.secure
# azul.api (AzulAPI instance), azul.settings (AzulSettings instance)
PyAzul uses custom exceptions inheriting from pyazul.AzulError
:
pyazul.core.exceptions.SSLError
: SSL configuration issues.pyazul.core.exceptions.APIError
: General HTTP/API communication problems.pyazul.core.exceptions.AzulResponseError
: Azul API returned an error (e.g., transaction declined). Hasresponse_data
attribute.
from pyazul import AzulError, AzulResponseError
from pyazul.core.exceptions import APIError, SSLError
try:
# response = await azul.sale({...})
pass # Your PyAzul call
except AzulResponseError as e:
print(f"Azul API Error: {e.message}, Response: {e.response_data}")
except APIError as e: # Covers other API communication issues (network, etc.)
print(f"API Communication Error: {e}")
except SSLError as e:
print(f"SSL Config Error: {e}")
except AzulError as e: # Catch-all for other PyAzul specific errors
print(f"PyAzul SDK Error: {e}")
except Exception as e: # Other unexpected errors
print(f"An unexpected error occurred: {e}")
For official Azul API documentation, visit the Azul Developer Portal (link subject to change).
© MIT License