{"id":21020373,"url":"https://github.com/mariofix/merchants","last_synced_at":"2026-03-10T00:09:31.948Z","repository":{"id":167770814,"uuid":"625437493","full_name":"mariofix/merchants","owner":"mariofix","description":"A unified payment processing toolkit","archived":false,"fork":false,"pushed_at":"2026-02-23T08:54:05.000Z","size":201,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"sandbox","last_synced_at":"2026-02-23T15:34:00.584Z","etag":null,"topics":["payment-gateway","payment-integration","payment-processing","payments","payments-platform","python"],"latest_commit_sha":null,"homepage":"https://mariofix.github.io/merchants","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mariofix.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["mariofix"]}},"created_at":"2023-04-09T05:17:56.000Z","updated_at":"2026-02-23T08:54:09.000Z","dependencies_parsed_at":"2023-09-26T05:06:44.521Z","dependency_job_id":"3cdf691c-f9a2-4ac7-9aa2-ed71fc6f2729","html_url":"https://github.com/mariofix/merchants","commit_stats":null,"previous_names":["mariofix/merchants"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mariofix/merchants","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariofix%2Fmerchants","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariofix%2Fmerchants/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariofix%2Fmerchants/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariofix%2Fmerchants/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mariofix","download_url":"https://codeload.github.com/mariofix/merchants/tar.gz/refs/heads/sandbox","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mariofix%2Fmerchants/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30317805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T20:05:46.299Z","status":"ssl_error","status_checked_at":"2026-03-09T19:57:04.425Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["payment-gateway","payment-integration","payment-processing","payments","payments-platform","python"],"created_at":"2024-11-19T10:40:00.388Z","updated_at":"2026-03-10T00:09:31.940Z","avatar_url":"https://github.com/mariofix.png","language":"Python","funding_links":["https://github.com/sponsors/mariofix"],"categories":[],"sub_categories":[],"readme":"# merchants\n\nA framework-agnostic Python SDK for hosted-checkout payment flows.\n\n## Features\n\n- **Hosted checkout only** – redirect users to a provider-hosted payment page; no card data ever touches your server.\n- **Built-in providers** – Stripe, PayPal, [Flow.cl](https://www.flow.cl) (`pip install merchants[flow]`), [Khipu](https://khipu.com) (`pip install merchants[khipu]`), and a `DummyProvider` for local dev.\n- **Provider metadata** – every provider exposes `name`, `author`, `version`, `description`, and `url` via `ProviderInfo` (Pydantic model), enabling downstream applications to inspect and parse the registry.\n- **CLI** – a Typer-powered command-line interface for listing providers and inspecting their metadata (`pip install \"merchants[cli]\"`).\n- **Pluggable transport** – default `requests.Session` backend; inject any `Transport` (e.g. httpx) for testing or custom HTTP clients.\n- **Flexible auth** – API-key header auth and token (Bearer) auth strategies.\n- **Pydantic models** – `CheckoutSession`, `PaymentStatus`, `WebhookEvent` with full type hints.\n- **Amount helpers** – `to_decimal_string`, `to_minor_units`, `from_minor_units`.\n- **Webhook utilities** – HMAC-SHA256 constant-time signature verification and best-effort event parsing.\n\n## Requirements\n\n- Python ≥ 3.10\n- `pydantic \u003e= 2.0`\n- `requests \u003e= 2.28`\n\n## Installation\n\n```bash\npip install merchants              # core (Stripe + PayPal stubs)\npip install \"merchants[flow]\"      # + Flow.cl via pyflowcl\npip install \"merchants[khipu]\"     # + Khipu via khipu-tools\npip install \"merchants[cli]\"       # + CLI (typer)\npip install -e \".[dev]\"            # local development\n```\n\n## Quick Start\n\n```python\nimport merchants\nfrom merchants.providers.stripe import StripeProvider\n\n# 1. Create a provider\nstripe = StripeProvider(api_key=\"sk_test_…\")\n\n# 2. Create a client (accepts provider instance or registered key string)\nclient = merchants.Client(provider=stripe)\n\n# 3. Create a hosted checkout session – raises UserError on failure\ntry:\n    session = client.payments.create_checkout(\n        amount=\"19.99\",\n        currency=\"USD\",\n        success_url=\"https://example.com/success\",\n        cancel_url=\"https://example.com/cancel\",\n        metadata={\"order_id\": \"ord_123\"},\n    )\n    print(session.redirect_url)  # redirect your user here\nexcept merchants.UserError as e:\n    print(\"Payment error:\", e)\n```\n\n## Providers\n\n| Provider | Key | Install extra | Notes |\n|---|---|---|---|\n| `StripeProvider` | `\"stripe\"` | – | Minor-unit amounts (cents) |\n| `PayPalProvider` | `\"paypal\"` | – | Decimal-string amounts |\n| `FlowProvider` | `\"flow\"` | `merchants[flow]` | Flow.cl (Chile) via `pyflowcl` |\n| `KhipuProvider` | `\"khipu\"` | `merchants[khipu]` | Khipu (Chile) via `khipu-tools` |\n| `GenericProvider` | `\"generic\"` | – | Configurable REST endpoints |\n| `DummyProvider` | `\"dummy\"` | – | Random data, no API calls |\n\n```python\n# Stripe\nfrom merchants.providers.stripe import StripeProvider\nclient = Client(provider=StripeProvider(api_key=\"sk_test_…\"))\n\n# PayPal\nfrom merchants.providers.paypal import PayPalProvider\nclient = Client(provider=PayPalProvider(access_token=\"token_…\"))\n\n# Flow.cl  (pip install merchants[flow])\nfrom merchants.providers.flow import FlowProvider\nclient = Client(provider=FlowProvider(api_key=\"…\", api_secret=\"…\"))\n\n# Khipu  (pip install merchants[khipu])\nfrom merchants.providers.khipu import KhipuProvider\nclient = Client(provider=KhipuProvider(api_key=\"…\"))\n\n# Dummy – no credentials, random data for local dev\nfrom merchants.providers.dummy import DummyProvider\nclient = Client(provider=DummyProvider())\n```\n\n## Provider Selection\n\n### By instance\n\n```python\nfrom merchants import Client\nfrom merchants.providers.paypal import PayPalProvider\n\nclient = Client(provider=PayPalProvider(access_token=\"token_…\"))\n```\n\n### By string key (registry)\n\n```python\nfrom merchants import Client, register_provider\nfrom merchants.providers.stripe import StripeProvider\n\n# Register once at startup\nregister_provider(StripeProvider(api_key=\"sk_test_…\"))\n\n# Later, select by key\nclient = Client(provider=\"stripe\")\n```\n\n### List registered providers\n\n```python\nfrom merchants import list_providers\n\nprint(list_providers())   # ['stripe', 'paypal', ...]\n```\n\n### Custom provider\n\nSee `examples/03_custom_provider.py` for a full example.\n\n```python\nfrom merchants.providers import Provider, UserError\nfrom merchants.models import CheckoutSession, PaymentStatus, PaymentState, WebhookEvent\n\nclass MyProvider(Provider):\n    key = \"my_gateway\"\n    name = \"My Gateway\"\n    author = \"acme\"\n    version = \"1.0.0\"\n    description = \"Custom in-house payment gateway\"\n    url = \"https://my-gateway.example.com\"\n\n    def create_checkout(self, amount, currency, success_url, cancel_url, metadata=None):\n        # Call your gateway here; raise UserError on failure\n        return CheckoutSession(\n            session_id=\"sess_1\",\n            redirect_url=\"https://pay.my-gateway.com/sess_1\",\n            provider=self.key,\n            amount=amount,\n            currency=currency,\n        )\n\n    def get_payment(self, payment_id):\n        return PaymentStatus(payment_id=payment_id, state=PaymentState.PENDING, provider=self.key)\n\n    def parse_webhook(self, payload, headers):\n        from merchants.webhooks import parse_event\n        return parse_event(payload, provider=self.key)\n```\n\n## Provider Metadata\n\nEvery provider exposes structured metadata through the `ProviderInfo` Pydantic model.\nDownstream applications can inspect the registry, serialise it to JSON, or drive\nrouting logic without knowing provider implementation details.\n\n### Required fields for new providers\n\n| Field | Type | Description |\n|---|---|---|\n| `key` | `str` | Short machine-readable identifier (e.g. `\"stripe\"`) |\n| `name` | `str` | Human-readable name (e.g. `\"Stripe\"`) |\n| `author` | `str` | Author/maintainer of the integration |\n| `version` | `str` | Version string for this integration |\n| `description` | `str` | Short description *(optional, defaults to `\"\"`)* |\n| `url` | `str` | Homepage or docs URL *(optional, defaults to `\"\"`)* |\n\n### Inspecting a single provider\n\n```python\nfrom merchants.providers.dummy import DummyProvider\nimport merchants\n\nprovider = DummyProvider()\ninfo = provider.get_info()   # returns a ProviderInfo pydantic model\n\nprint(info.key)          # \"dummy\"\nprint(info.name)         # \"Dummy\"\nprint(info.author)       # \"merchants team\"\nprint(info.model_dump()) # {'key': 'dummy', 'name': 'Dummy', ...}\nprint(info.model_dump_json(indent=2))  # JSON string\n```\n\n### Inspecting all registered providers\n\n```python\nfrom merchants import register_provider, describe_providers\nfrom merchants.providers.dummy import DummyProvider\nfrom merchants.providers.stripe import StripeProvider\n\nregister_provider(DummyProvider())\nregister_provider(StripeProvider(api_key=\"sk_test_…\"))\n\nfor info in describe_providers():\n    print(f\"{info.key}: {info.name} v{info.version}\")\n# dummy: Dummy v1.0.0\n# stripe: Stripe v1.0.0\n\n# Serialise the entire registry to JSON\nimport json\nprint(json.dumps([i.model_dump() for i in describe_providers()], indent=2))\n```\n\n## CLI\n\nInstall the CLI extra and use the `merchants` command:\n\n```bash\npip install \"merchants[cli]\"\nmerchants --help\n```\n\n```\n merchants – framework-agnostic hosted-checkout payment SDK.\n\n╭─ Commands ───────────────────────────────────────────────────────────╮\n│ version     Show the merchants package version.                      │\n│ providers   List all registered payment providers.                   │\n│ info        Show metadata for a registered provider.                 │\n│ payments    Create checkout sessions, retrieve payment status, …     │\n╰──────────────────────────────────────────────────────────────────────╯\n```\n\n**Show the package version:**\n\n```bash\nmerchants version\n# merchants 0.1.0\n```\n\n**List registered providers (table or JSON):**\n\n```bash\nmerchants providers\n# Key          Name       Author           Version\n# -------------------------------------------------------\n# dummy        Dummy      merchants team   1.0.0\n\nmerchants providers --output json\n# [{\"key\": \"dummy\", \"name\": \"Dummy\", ...}]\n```\n\n**Show metadata for a specific provider:**\n\n```bash\nmerchants info dummy\n# Key         : dummy\n# Name        : Dummy\n# Author      : merchants team\n# Version     : 1.0.0\n# Description : Local development provider …\n# URL         :\n\nmerchants info stripe --output json\n# {\"key\": \"stripe\", \"name\": \"Stripe\", ...}\n```\n\n**Create a checkout session:**\n\n```bash\n# DummyProvider – no credentials needed, great for testing\nmerchants payments checkout \\\n  --provider dummy \\\n  --amount 19.99 \\\n  --currency USD \\\n  --success-url https://example.com/ok \\\n  --cancel-url https://example.com/cancel\n# Session ID  : dummy_sess_abc123\n# Redirect URL: https://dummy-pay.example.com/pay/dummy_sess_abc123?...\n# Provider    : dummy\n# Amount      : 19.99 USD\n\n# With metadata and JSON output\nmerchants payments checkout \\\n  --provider dummy \\\n  --amount 49.99 \\\n  --currency EUR \\\n  --success-url https://example.com/ok \\\n  --cancel-url https://example.com/cancel \\\n  --metadata '{\"order_id\": \"ORD-42\"}' \\\n  --output json\n```\n\nBuilt-in providers read credentials from environment variables:\n\n| Provider | Environment variable(s) |\n|---|---|\n| `stripe` | `STRIPE_API_KEY` |\n| `paypal` | `PAYPAL_ACCESS_TOKEN` |\n| `generic` | `GENERIC_CHECKOUT_URL`, `GENERIC_PAYMENT_URL` |\n\n```bash\nSTRIPE_API_KEY=sk_test_… merchants payments checkout \\\n  --provider stripe --amount 9.99 --currency USD \\\n  --success-url https://example.com/ok \\\n  --cancel-url https://example.com/cancel\n```\n\n**Get payment status:**\n\n```bash\nmerchants payments get pay_abc123 --provider dummy\n# Payment ID  : pay_abc123\n# State       : succeeded\n# Provider    : dummy\n# Final       : yes\n# Success     : yes\n\nmerchants payments get pay_abc123 --provider dummy --output json\n```\n\n**Parse and verify a webhook:**\n\n```bash\n# Parse a webhook payload from a file\nmerchants payments webhook --file payload.json --provider stripe\n\n# Verify HMAC-SHA256 signature before parsing\nmerchants payments webhook \\\n  --file payload.json \\\n  --provider stripe \\\n  --secret whsec_… \\\n  --signature \"sha256=abc123…\"\n# Event ID    : evt_…\n# Event Type  : payment_intent.succeeded\n# Payment ID  : pi_…\n# State       : succeeded\n# Provider    : stripe\n# Verified    : yes\n\n# Or pipe from stdin\ncat payload.json | merchants payments webhook --provider stripe\n```\n\n## Checkout Creation\n\n```python\ntry:\n    session = client.payments.create_checkout(\n        amount=\"99.00\",\n        currency=\"EUR\",\n        success_url=\"https://shop.example.com/thank-you\",\n        cancel_url=\"https://shop.example.com/cart\",\n    )\n    return redirect(session.redirect_url)\nexcept merchants.UserError as e:\n    return f\"Payment setup failed: {e}\", 400\n```\n\n## Payment Status\n\n```python\nstatus = client.payments.get(\"pi_3LHpu2…\")\n\nprint(status.state)        # e.g. PaymentState.SUCCEEDED\nprint(status.is_final)     # True once payment is terminal\nprint(status.is_success)   # True only when SUCCEEDED\n```\n\n## Webhook Verification \u0026 Parsing\n\n```python\nimport merchants\n\n# 1. Verify signature (constant-time HMAC-SHA256)\ntry:\n    merchants.verify_signature(\n        payload=request.body,          # raw bytes\n        secret=\"whsec_…\",\n        signature=request.headers[\"Stripe-Signature\"],\n    )\nexcept merchants.WebhookVerificationError:\n    return 400  # reject\n\n# 2. Parse and normalise the event\nevent = merchants.parse_event(request.body, provider=\"stripe\")\n\nprint(event.event_type)  # e.g. \"payment_intent.succeeded\"\nprint(event.state)       # e.g. PaymentState.SUCCEEDED\nprint(event.payment_id)  # e.g. \"pi_3LHpu2…\"\n```\n\n## Amount Format Notes\n\n| Helper | Example | Use case |\n|---|---|---|\n| `to_decimal_string(\"19.99\")` | `\"19.99\"` | PayPal, most REST APIs |\n| `to_minor_units(\"19.99\")` | `1999` | Stripe (cents/pence) |\n| `from_minor_units(1999)` | `Decimal(\"19.99\")` | Converting Stripe amounts back |\n\n```python\nfrom merchants import to_decimal_string, to_minor_units, from_minor_units\nfrom decimal import Decimal\n\nto_decimal_string(Decimal(\"9.5\"))   # \"9.50\"\nto_minor_units(\"19.99\")             # 1999\nto_minor_units(\"1000\", decimals=0)  # 1000  (JPY, no cents)\nfrom_minor_units(1999)              # Decimal(\"19.99\")\n```\n\n## Auth Strategies\n\n```python\nfrom merchants import Client, ApiKeyAuth, TokenAuth\nfrom merchants.providers.generic import GenericProvider\n\n# API key header\nclient = Client(\n    provider=GenericProvider(\"https://api.example.com/checkout\", \"https://api.example.com/payments/{payment_id}\"),\n    auth=ApiKeyAuth(\"my-key\", header=\"X-API-Key\"),\n)\n\n# Bearer token\nclient = Client(\n    provider=...,\n    auth=TokenAuth(\"my-token\"),   # Authorization: Bearer my-token\n)\n```\n\n## Custom Transport\n\n```python\nfrom merchants import Client, RequestsTransport\n\n# Inject a pre-configured requests.Session (e.g. with retries)\nimport requests\nfrom requests.adapters import HTTPAdapter, Retry\n\nsession = requests.Session()\nretry = Retry(total=3, backoff_factor=0.5)\nsession.mount(\"https://\", HTTPAdapter(max_retries=retry))\n\nclient = Client(\n    provider=\"stripe\",\n    transport=RequestsTransport(session=session),\n)\n```\n\n## Low-level Escape Hatch\n\n```python\nresponse = client.request(\"GET\", \"https://api.stripe.com/v1/balance\")\nprint(response.status_code, response.body)\n```\n\n## Examples\n\nThe `examples/` directory contains runnable scripts:\n\n| File | Description |\n|---|---|\n| `01_simple_client.py` | Basic client setup with DummyProvider and Stripe |\n| `02_custom_httpx_transport.py` | Custom httpx-backed transport |\n| `03_custom_provider.py` | Building your own provider |\n\n## Development\n\n```bash\npip install -e \".[dev]\"\npytest\n```\n\nThe package version is maintained in a single place: `src/merchants/version.py`.\nUpdate `__version__` there and it propagates to the package metadata, `merchants.__version__`,\nand the `merchants version` CLI command automatically.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmariofix%2Fmerchants","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmariofix%2Fmerchants","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmariofix%2Fmerchants/lists"}