FastAPI example implementation¶
This example shows the recommended pattern for using pagopar-sdk inside an async FastAPI app.
Because the SDK is synchronous (backed by httpx), each call is offloaded to a thread
with asyncio.to_thread so it never blocks the event loop.
Installation¶
pip install pagopar-sdk fastapi uvicorn
Or with uv:
uv add pagopar-sdk fastapi uvicorn
1) Service layer¶
All Pagopar calls live in a single module. Each helper is async and wraps the
sync SDK call with asyncio.to_thread.
# app/pagopar.py
import asyncio
import os
from datetime import datetime, timedelta
from pagopar_sdk import PagoparClient
PAGOPAR_CHECKOUT_BASE = "https://www.pagopar.com/pagos"
PUBLIC_KEY: str = os.getenv("PAGOPAR_PUBLIC_KEY", "")
PRIVATE_KEY: str = os.getenv("PAGOPAR_PRIVATE_KEY", "")
def _client(*, raise_on_api_error: bool = True) -> PagoparClient:
return PagoparClient(
public_key=PUBLIC_KEY,
private_key=PRIVATE_KEY,
raise_on_api_error=raise_on_api_error,
)
def checkout_url(data_hash: str) -> str:
return f"{PAGOPAR_CHECKOUT_BASE}/{data_hash}"
async def get_formas_pago() -> list[dict]:
"""List all available payment methods for the configured merchant."""
def _call():
with _client() as c:
data = c.commerce.get_payment_methods()
return data["resultado"]
return await asyncio.to_thread(_call)
async def iniciar_transaccion(
id_pedido_comercio: str,
monto_total: float,
comprador: dict,
compras_items: list[dict],
forma_pago: int | None = None,
descripcion_resumen: str = "",
fecha_maxima_pago: str | None = None,
) -> dict:
"""
Create a transaction at Pagopar (Step 1 of the purchase flow).
Returns {"data": "<checkout_hash>", "pedido": "<pagopar_order_id>"}.
After this, redirect the user to checkout_url(data["data"]).
"""
if not fecha_maxima_pago:
fecha_maxima_pago = (datetime.utcnow() + timedelta(hours=24)).strftime(
"%Y-%m-%d %H:%M:%S"
)
payload: dict = {
"id_pedido_comercio": id_pedido_comercio,
"monto_total": round(monto_total),
"tipo_pedido": "VENTA-COMERCIO",
"comprador": comprador,
"compras_items": compras_items,
"fecha_maxima_pago": fecha_maxima_pago,
"descripcion_resumen": descripcion_resumen,
}
if forma_pago is not None:
payload["forma_pago"] = forma_pago
def _call():
with _client() as c:
data = c.commerce.create_transaction(payload)
return data["resultado"][0]
return await asyncio.to_thread(_call)
async def consultar_pedido(hash_pedido: str) -> dict:
"""Query the current status of a Pagopar order by its hash."""
def _call():
with _client() as c:
data = c.commerce.get_order(hash_pedido)
return data["resultado"][0]
return await asyncio.to_thread(_call)
async def reversar_pedido(hash_pedido: str) -> dict:
"""Request a reversal/refund for a Pagopar order."""
def _call():
# raise_on_api_error=False so callers can inspect respuesta themselves
with _client(raise_on_api_error=False) as c:
return c.commerce.reverse_order(hash_pedido)
return await asyncio.to_thread(_call)
2) Routes¶
# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from . import pagopar
app = FastAPI(title="Pagopar Integration API")
@app.get("/payments/methods")
async def payment_methods():
return await pagopar.get_formas_pago()
class TransactionRequest(BaseModel):
id_pedido_comercio: str
monto_total: float
comprador: dict
compras_items: list[dict]
forma_pago: int | None = None
descripcion_resumen: str = ""
fecha_maxima_pago: str | None = None
@app.post("/payments/transaction")
async def create_transaction(body: TransactionRequest):
result = await pagopar.iniciar_transaccion(**body.model_dump())
return {
"checkout_url": pagopar.checkout_url(result["data"]),
"pedido": result["pedido"],
}
@app.get("/payments/order/{hash_pedido}")
async def get_order(hash_pedido: str):
return await pagopar.consultar_pedido(hash_pedido)
@app.post("/payments/order/{hash_pedido}/reverse")
async def reverse_order(hash_pedido: str):
result = await pagopar.reversar_pedido(hash_pedido)
if not result.get("respuesta"):
raise HTTPException(status_code=400, detail=result)
return result
3) Run¶
uvicorn app.main:app --reload
Good practices¶
- The SDK is synchronous — always wrap calls with
asyncio.to_threadto avoid blocking the event loop. - Use
raise_on_api_error=Falsewhen you need to inspect the rawrespuesta(e.g. reversals), and raiseHTTPExceptionmanually. - Keep environment variables (
PAGOPAR_PUBLIC_KEY,PAGOPAR_PRIVATE_KEY) out of source control; load them withos.getenvorpydantic-settings. monto_totalis sent as an integer (guaraníes have no cents); alwaysround()before sending.- After
iniciar_transaccion, redirect the user tocheckout_url(result["data"])to complete payment.