Skip to main content

Framework-agnostic hosted-checkout payment SDK

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[flow]), Khipu (pip install merchants[khipu]), and a DummyProvider for local dev.
  • Provider metadata – every provider exposes name, author, version, description, and url via ProviderInfo (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[cli]").
  • 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.
  • Pydantic modelsCheckoutSession, PaymentStatus, WebhookEvent with full type hints.
  • Amount helpersto_decimal_string, to_minor_units, from_minor_units.
  • Webhook utilities – HMAC-SHA256 constant-time signature verification and best-effort event parsing.

Requirements

  • Python ≥ 3.10
  • pydantic >= 2.0
  • requests >= 2.28

Installation

pip install merchants              # core (Stripe + PayPal stubs)
pip install "merchants[flow]"      # + Flow.cl via pyflowcl
pip install "merchants[khipu]"     # + Khipu via khipu-tools
pip install "merchants[cli]"       # + CLI (typer)
pip install -e ".[dev]"            # local development

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[flow])
from merchants.providers.flow import FlowProvider
client = Client(provider=FlowProvider(api_key="…", api_secret="…"))

# Khipu  (pip install merchants[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[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

Development

pip install -e ".[dev]"
pytest

The package version is maintained in a single place: src/merchants/version.py. Update __version__ there and it propagates to the package metadata, merchants.__version__, and the merchants version CLI command automatically.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

merchants_sdk-2026.2.3.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

merchants_sdk-2026.2.3-py3-none-any.whl (30.5 kB view details)

Uploaded Python 3

File details

Details for the file merchants_sdk-2026.2.3.tar.gz.

File metadata

  • Download URL: merchants_sdk-2026.2.3.tar.gz
  • Upload date:
  • Size: 23.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.1 CPython/3.10.6 Linux/6.8.0-101-generic

File hashes

Hashes for merchants_sdk-2026.2.3.tar.gz
Algorithm Hash digest
SHA256 f75457670ac23cacc74de031a1197630464f3b3dbff1cb2f45fc5087977f0bea
MD5 0f04052a475953e47a09cab7f4fc1a5c
BLAKE2b-256 56b0b1b57ac4929b13e42d95f328876ec5e5ecde53615b090f60a86fa237a9f9

See more details on using hashes here.

File details

Details for the file merchants_sdk-2026.2.3-py3-none-any.whl.

File metadata

  • Download URL: merchants_sdk-2026.2.3-py3-none-any.whl
  • Upload date:
  • Size: 30.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.1 CPython/3.10.6 Linux/6.8.0-101-generic

File hashes

Hashes for merchants_sdk-2026.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 aac8a78d809ca152c2297ada824a9c448217492c35c4db2142373b4dd7f22cb4
MD5 142005b816c0329a3196c55e3e566eef
BLAKE2b-256 14e0e73ab21f0c83d6a3b4c4942fea136295cffbe649da08bf9d355810b06120

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page