Payments for people who have better things to do.
- Hosted checkout only – redirect users to a provider-hosted payment page; no card data ever touches your server.
- Built-in providers – Stripe, PayPal, Flow.cl (
pip install merchants-sdk[flow]orpip install pyflowcl), Khipu (pip install merchants-sdk[khipu]orpip install khipu-tools), and aDummyProviderfor local dev. - Pluggable transport – default
requests.Sessionbackend; inject anyTransport(e.g. httpx) for testing or custom HTTP clients. - Flexible auth – API-key header auth and token (Bearer) auth strategies.
pip install merchants-sdk # core (Stripe, PayPal, Flow, Khipu)import merchants
from merchants.providers.stripe import StripeProvider
# 1. Create a provider
stripe = StripeProvider(api_key="sk_test_…")
# 2. Create a client (accepts provider instance or registered key string)
client = merchants.Client(provider=stripe)
# 3. Create a hosted checkout session – raises UserError on failure
try:
session = client.payments.create_checkout(
amount="19.99",
currency="USD",
success_url="https://example.com/success",
cancel_url="https://example.com/cancel",
metadata={"order_id": "ord_123"},
)
print(session.redirect_url) # redirect your user here
except merchants.UserError as e:
print("Payment error:", e)| Provider | Key | Install extra | Notes |
|---|---|---|---|
StripeProvider |
"stripe" |
– | Minor-unit amounts (cents) |
PayPalProvider |
"paypal" |
– | Decimal-string amounts |
FlowProvider |
"flow" |
merchants[flow] |
Flow.cl (Chile) via pyflowcl |
KhipuProvider |
"khipu" |
merchants[khipu] |
Khipu (Chile) via khipu-tools |
GenericProvider |
"generic" |
– | Configurable REST endpoints |
DummyProvider |
"dummy" |
– | Random data, no API calls |
# Stripe
from merchants.providers.stripe import StripeProvider
client = Client(provider=StripeProvider(api_key="sk_test_…"))
# PayPal
from merchants.providers.paypal import PayPalProvider
client = Client(provider=PayPalProvider(access_token="token_…"))
# Flow.cl (pip install merchants-sdk[flow])
from merchants.providers.flow import FlowProvider
client = Client(provider=FlowProvider(api_key="…", api_secret="…"))
# Khipu (pip install merchants-sdk[khipu])
from merchants.providers.khipu import KhipuProvider
client = Client(provider=KhipuProvider(api_key="…"))
# Dummy – no credentials, random data for local dev
from merchants.providers.dummy import DummyProvider
client = Client(provider=DummyProvider())from merchants import Client
from merchants.providers.paypal import PayPalProvider
client = Client(provider=PayPalProvider(access_token="token_…"))from merchants import Client, register_provider
from merchants.providers.stripe import StripeProvider
# Register once at startup
register_provider(StripeProvider(api_key="sk_test_…"))
# Later, select by key
client = Client(provider="stripe")from merchants import list_providers
print(list_providers()) # ['stripe', 'paypal', ...]See examples/03_custom_provider.py for a full example.
from merchants.providers import Provider, UserError
from merchants.models import CheckoutSession, PaymentStatus, PaymentState, WebhookEvent
class MyProvider(Provider):
key = "my_gateway"
name = "My Gateway"
author = "acme"
version = "1.0.0"
description = "Custom in-house payment gateway"
url = "https://my-gateway.example.com"
def create_checkout(self, amount, currency, success_url, cancel_url, metadata=None):
# Call your gateway here; raise UserError on failure
return CheckoutSession(
session_id="sess_1",
redirect_url="https://pay.my-gateway.com/sess_1",
provider=self.key,
amount=amount,
currency=currency,
)
def get_payment(self, payment_id):
return PaymentStatus(payment_id=payment_id, state=PaymentState.PENDING, provider=self.key)
def parse_webhook(self, payload, headers):
from merchants.webhooks import parse_event
return parse_event(payload, provider=self.key)Every provider exposes structured metadata through the ProviderInfo Pydantic model.
Downstream applications can inspect the registry, serialise it to JSON, or drive
routing logic without knowing provider implementation details.
| Field | Type | Description |
|---|---|---|
key |
str |
Short machine-readable identifier (e.g. "stripe") |
name |
str |
Human-readable name (e.g. "Stripe") |
author |
str |
Author/maintainer of the integration |
version |
str |
Version string for this integration |
description |
str |
Short description (optional, defaults to "") |
url |
str |
Homepage or docs URL (optional, defaults to "") |
from merchants.providers.dummy import DummyProvider
import merchants
provider = DummyProvider()
info = provider.get_info() # returns a ProviderInfo pydantic model
print(info.key) # "dummy"
print(info.name) # "Dummy"
print(info.author) # "merchants team"
print(info.model_dump()) # {'key': 'dummy', 'name': 'Dummy', ...}
print(info.model_dump_json(indent=2)) # JSON stringfrom merchants import register_provider, describe_providers
from merchants.providers.dummy import DummyProvider
from merchants.providers.stripe import StripeProvider
register_provider(DummyProvider())
register_provider(StripeProvider(api_key="sk_test_…"))
for info in describe_providers():
print(f"{info.key}: {info.name} v{info.version}")
# dummy: Dummy v1.0.0
# stripe: Stripe v1.0.0
# Serialise the entire registry to JSON
import json
print(json.dumps([i.model_dump() for i in describe_providers()], indent=2))try:
session = client.payments.create_checkout(
amount="99.00",
currency="EUR",
success_url="https://shop.example.com/thank-you",
cancel_url="https://shop.example.com/cart",
)
return redirect(session.redirect_url)
except merchants.UserError as e:
return f"Payment setup failed: {e}", 400status = client.payments.get("pi_3LHpu2…")
print(status.state) # e.g. PaymentState.SUCCEEDED
print(status.is_final) # True once payment is terminal
print(status.is_success) # True only when SUCCEEDEDimport merchants
# 1. Verify signature (constant-time HMAC-SHA256)
try:
merchants.verify_signature(
payload=request.body, # raw bytes
secret="whsec_…",
signature=request.headers["Stripe-Signature"],
)
except merchants.WebhookVerificationError:
return 400 # reject
# 2. Parse and normalise the event
event = merchants.parse_event(request.body, provider="stripe")
print(event.event_type) # e.g. "payment_intent.succeeded"
print(event.state) # e.g. PaymentState.SUCCEEDED
print(event.payment_id) # e.g. "pi_3LHpu2…"| Helper | Example | Use case |
|---|---|---|
to_decimal_string("19.99") |
"19.99" |
PayPal, most REST APIs |
to_minor_units("19.99") |
1999 |
Stripe (cents/pence) |
from_minor_units(1999) |
Decimal("19.99") |
Converting Stripe amounts back |
from merchants import to_decimal_string, to_minor_units, from_minor_units
from decimal import Decimal
to_decimal_string(Decimal("9.5")) # "9.50"
to_minor_units("19.99") # 1999
to_minor_units("1000", decimals=0) # 1000 (JPY, no cents)
from_minor_units(1999) # Decimal("19.99")from merchants import Client, ApiKeyAuth, TokenAuth
from merchants.providers.generic import GenericProvider
# API key header
client = Client(
provider=GenericProvider("https://api.example.com/checkout", "https://api.example.com/payments/{payment_id}"),
auth=ApiKeyAuth("my-key", header="X-API-Key"),
)
# Bearer token
client = Client(
provider=...,
auth=TokenAuth("my-token"), # Authorization: Bearer my-token
)from merchants import Client, RequestsTransport
# Inject a pre-configured requests.Session (e.g. with retries)
import requests
from requests.adapters import HTTPAdapter, Retry
session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5)
session.mount("https://", HTTPAdapter(max_retries=retry))
client = Client(
provider="stripe",
transport=RequestsTransport(session=session),
)response = client.request("GET", "https://api.stripe.com/v1/balance")
print(response.status_code, response.body)The examples/ directory contains runnable scripts:
| File | Description |
|---|---|
01_simple_client.py |
Basic client setup with DummyProvider and Stripe |
02_custom_httpx_transport.py |
Custom httpx-backed transport |
03_custom_provider.py |
Building your own provider |