Payments for people who have better things to do.
Project description
merchants
A framework-agnostic Python SDK for hosted-checkout payment flows.
Features
- 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]), Khipu (pip install merchants-sdk[khipu]), and aDummyProviderfor local dev. - Provider metadata – every provider exposes
name,author,version,description, andurlviaProviderInfo(Pydantic model), enabling downstream applications to inspect and parse the registry. - CLI – a Typer-powered command-line interface for listing providers and inspecting their metadata (
pip install merchants-sdk[cli]). - 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.
- Pydantic models –
CheckoutSession,PaymentStatus,WebhookEventwith full type hints. - Amount helpers –
to_decimal_string,to_minor_units,from_minor_units.
Installation
pip install merchants-sdk # core (Stripe + PayPal stubs)
pip install "merchants-sdk[cli]" # + CLI (typer)
Quick Start
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)
Providers
| 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())
Provider Selection
By instance
from merchants import Client
from merchants.providers.paypal import PayPalProvider
client = Client(provider=PayPalProvider(access_token="token_…"))
By string key (registry)
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")
List registered providers
from merchants import list_providers
print(list_providers()) # ['stripe', 'paypal', ...]
Custom provider
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)
Provider Metadata
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.
Required fields for new providers
| 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 "") |
Inspecting a single provider
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 string
Inspecting all registered providers
from 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))
CLI
Install the CLI extra and use the merchants command:
pip install "merchants-sdk[cli]"
merchants --help
merchants – framework-agnostic hosted-checkout payment SDK.
╭─ Commands ───────────────────────────────────────────────────────────╮
│ version Show the merchants package version. │
│ providers List all registered payment providers. │
│ info Show metadata for a registered provider. │
│ payments Create checkout sessions, retrieve payment status, … │
╰──────────────────────────────────────────────────────────────────────╯
Show the package version:
merchants version
# merchants 0.1.0
List registered providers (table or JSON):
merchants providers
# Key Name Author Version
# -------------------------------------------------------
# dummy Dummy merchants team 1.0.0
merchants providers --output json
# [{"key": "dummy", "name": "Dummy", ...}]
Show metadata for a specific provider:
merchants info dummy
# Key : dummy
# Name : Dummy
# Author : merchants team
# Version : 1.0.0
# Description : Local development provider …
# URL :
merchants info stripe --output json
# {"key": "stripe", "name": "Stripe", ...}
Create a checkout session:
# DummyProvider – no credentials needed, great for testing
merchants payments checkout \
--provider dummy \
--amount 19.99 \
--currency USD \
--success-url https://example.com/ok \
--cancel-url https://example.com/cancel
# Session ID : dummy_sess_abc123
# Redirect URL: https://dummy-pay.example.com/pay/dummy_sess_abc123?...
# Provider : dummy
# Amount : 19.99 USD
# With metadata and JSON output
merchants payments checkout \
--provider dummy \
--amount 49.99 \
--currency EUR \
--success-url https://example.com/ok \
--cancel-url https://example.com/cancel \
--metadata '{"order_id": "ORD-42"}' \
--output json
Built-in providers read credentials from environment variables:
| Provider | Environment variable(s) |
|---|---|
stripe |
STRIPE_API_KEY |
paypal |
PAYPAL_ACCESS_TOKEN |
generic |
GENERIC_CHECKOUT_URL, GENERIC_PAYMENT_URL |
STRIPE_API_KEY=sk_test_… merchants payments checkout \
--provider stripe --amount 9.99 --currency USD \
--success-url https://example.com/ok \
--cancel-url https://example.com/cancel
Get payment status:
merchants payments get pay_abc123 --provider dummy
# Payment ID : pay_abc123
# State : succeeded
# Provider : dummy
# Final : yes
# Success : yes
merchants payments get pay_abc123 --provider dummy --output json
Parse and verify a webhook:
# Parse a webhook payload from a file
merchants payments webhook --file payload.json --provider stripe
# Verify HMAC-SHA256 signature before parsing
merchants payments webhook \
--file payload.json \
--provider stripe \
--secret whsec_… \
--signature "sha256=abc123…"
# Event ID : evt_…
# Event Type : payment_intent.succeeded
# Payment ID : pi_…
# State : succeeded
# Provider : stripe
# Verified : yes
# Or pipe from stdin
cat payload.json | merchants payments webhook --provider stripe
Checkout Creation
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}", 400
Payment Status
status = 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 SUCCEEDED
Webhook Verification & Parsing
import 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…"
Amount Format Notes
| 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")
Auth Strategies
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
)
Custom Transport
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),
)
Low-level Escape Hatch
response = client.request("GET", "https://api.stripe.com/v1/balance")
print(response.status_code, response.body)
Examples
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 |
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 merchants_sdk-2026.3.0.tar.gz.
File metadata
- Download URL: merchants_sdk-2026.3.0.tar.gz
- Upload date:
- Size: 25.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41f44ecf2a8f76ec450bd7fe53adf2c9ad331cb69a0b17d6d679c98911d254e8
|
|
| MD5 |
72ce21044f66ad76918669fcbd0f2d92
|
|
| BLAKE2b-256 |
c880016f1b2c772f5845303d3005f1b7412be0dae12f40dbdf68361050b87a57
|
Provenance
The following attestation bundles were made for merchants_sdk-2026.3.0.tar.gz:
Publisher:
publish.yml on mariofix/merchants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
merchants_sdk-2026.3.0.tar.gz -
Subject digest:
41f44ecf2a8f76ec450bd7fe53adf2c9ad331cb69a0b17d6d679c98911d254e8 - Sigstore transparency entry: 1068902990
- Sigstore integration time:
-
Permalink:
mariofix/merchants@b64c0b45c71797f2b841cc960e4b75571ff2213e -
Branch / Tag:
refs/tags/v2026.3.0 - Owner: https://github.com/mariofix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b64c0b45c71797f2b841cc960e4b75571ff2213e -
Trigger Event:
release
-
Statement type:
File details
Details for the file merchants_sdk-2026.3.0-py3-none-any.whl.
File metadata
- Download URL: merchants_sdk-2026.3.0-py3-none-any.whl
- Upload date:
- Size: 32.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13d6ca59277c8326a0a71a9a6adf92cd18ae5c7b63293b85cedec2e69849a8a7
|
|
| MD5 |
13f63f7e6d481f3732a607f95dc3177c
|
|
| BLAKE2b-256 |
81db091b8744fc73e6414c777a8560c315bb9ab5e55ed0f5beb84334e7d2079b
|
Provenance
The following attestation bundles were made for merchants_sdk-2026.3.0-py3-none-any.whl:
Publisher:
publish.yml on mariofix/merchants
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
merchants_sdk-2026.3.0-py3-none-any.whl -
Subject digest:
13d6ca59277c8326a0a71a9a6adf92cd18ae5c7b63293b85cedec2e69849a8a7 - Sigstore transparency entry: 1068903022
- Sigstore integration time:
-
Permalink:
mariofix/merchants@b64c0b45c71797f2b841cc960e4b75571ff2213e -
Branch / Tag:
refs/tags/v2026.3.0 - Owner: https://github.com/mariofix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b64c0b45c71797f2b841cc960e4b75571ff2213e -
Trigger Event:
release
-
Statement type: