Skip to main content

merchants

Payments for people who have better things to do.

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] or pip install pyflowcl), Khipu (pip install merchants-sdk[khipu] or pip install khipu-tools), and a DummyProvider for local dev.
  • Pluggable transport – default requests.Session backend; inject any Transport (e.g. httpx) for testing or custom HTTP clients.
  • Flexible auth – API-key header auth and token (Bearer) auth strategies.

Installation

pip install merchants-sdk              # core (Stripe, PayPal, Flow, Khipu)

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))

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

Metadata

Release files for merchants-sdk 2026.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for merchants-sdk 2026.5.1
File Size Uploaded Provenance Actions
merchants_sdk-2026.5.1.tar.gz 28.8 kB Without provenance Details for merchants_sdk-2026.5.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for merchants-sdk 2026.5.1
File Interpreter ABI Platform Actions
merchants_sdk-2026.5.1-py3-none-any.whl Python 3 none any Details for merchants_sdk-2026.5.1-py3-none-any.whl

Total release size: 66.4 kB

Release files / merchants_sdk-2026.5.1.tar.gz

Download URL merchants_sdk-2026.5.1.tar.gz
Size 28.8 kB
Tags Source
SHA-256 checksum
How to use checksums
34dce0294b2236f58fa0a6108ba36e18ae8578c839c2831587fb4b375e72accf
BLAKE2b-256 checksum
How to use checksums
13a77e8b0264856e1f174c660c49ca00f900dd023a432feb507a91a2e1c8a7a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.1 CPython/3.10.6 Linux/6.8.0-117-generic

Release files / merchants_sdk-2026.5.1-py3-none-any.whl

Download URL merchants_sdk-2026.5.1-py3-none-any.whl
Size 37.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e1c926c8b9eb678a21551c6902d4952169c6d2160b0c86dcec1c068801998a87
BLAKE2b-256 checksum
How to use checksums
fdd2dbc00111511b320f18e0c4c39a2ff2491cddf2188d059c559b0806a88b08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.3.1 CPython/3.10.6 Linux/6.8.0-117-generic

Security for merchants-sdk 2026.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page