Skip to content

Repository files navigation

Paymos Python SDK

Official Python 3.10+ client for the Paymos Merchant API. It includes HMAC request signing, invoices, withdrawals, balances, static per-customer wallets and their deposit feed, bounded cursor iterators, structured errors, safe retries, and raw-body webhook verification.

pip install paymos-sdk
import os

from paymos import Paymos

paymos = Paymos(
    api_key=os.environ["PAYMOS_API_KEY"],
    api_secret=os.environ["PAYMOS_API_SECRET"],
)
invoice = paymos.invoices.create(
    project_id="prj_...", amount="10.00", currency="USD",
    external_order_id="order_123",
)

Use decimal strings for money. list() returns one cursor page; iterate() follows cursors lazily and stops after 100 pages by default.

for invoice in paymos.invoices.iterate(status=["paid", "paid_over"], max_pages=10):
    print(invoice["invoice_id"])

A payment channel is one payer's reusable set of deposit addresses. Create it, read its rails, then poll the confirmed-deposit feed and persist the cursor:

channel = paymos.payment_channels.create(project_id="prj_...", external_id="customer_42")
for rail in channel["networks"]:
    print(rail["network"], rail["status"], rail.get("address", "not provisioned yet"))

feed = paymos.payment_channel_deposits.read(cursor=saved_cursor, limit=100)
for deposit in feed["items"]:
    credit(deposit)
save_cursor(feed["next_cursor"])

Four things that bite a caller who guesses. Repeating the same external_id returns the same channel — 200 instead of 201, and both bodies are a channel, so calling this on every checkout is safe: a repeat is not a duplicate and not an error. A rail's address is absent until that rail finishes provisioning and never changes once it appears, so absent means "not yet", not "no address". An absent minimum_deposit means "we cannot quote a minimum right now", never "there is no minimum" — reading it as zero is how a merchant accepts a deposit that lands below the live minimum and is never credited. And next_cursor is never empty, not even on a page with no items: store it and resume from it, and never loop until it is falsy the way iterate() ends a list, because that loop either spins forever or stops on the first quiet page and leaves reconciliation silently behind. confirmed_from is the first poll's lower bound only; afterwards the stored cursor is the resume mechanism.

API failures raise typed subclasses of ApiError and preserve the HTTP status, problem details, field, error code, headers, and Retry-After value.

Verify webhooks against the exact raw request body before parsing it:

from paymos import WebhookVerifier

event = WebhookVerifier(os.environ["PAYMOS_WEBHOOK_SECRET"]).construct_event(
    request.headers["X-Webhook-Signature"], request.get_data()
)

The three payment_channel.deposit.* events each carry a full payment-channel deposit as their data. construct_event returns it untyped, so bind it to PaymentChannelDeposit yourself:

from typing import cast

from paymos import PaymentChannelDeposit

if event["event_type"] == "payment_channel.deposit.confirmed":
    deposit = cast(PaymentChannelDeposit, event["data"])
    if deposit["is_final"]:
        credit(deposit["payment_channel_external_id"], deposit["net"])  # idempotent by event["event_id"]

confirming and reorged are advisory and may arrive out of order — a confirming can land after the confirmed for the same deposit, and a reorged can be superseded by a later confirmed. Credit only on payment_channel.deposit.confirmed with is_final true, and never let an advisory event regress a deposit already known to be confirmed: crediting on confirming releases goods against money a reorg can still take back.

Never use the API secret in browser or mobile code. Full documentation: https://paymos.io/docs/server-sdks

About

Accept USDT and USDC from Python. Official Paymos Merchant API client with HMAC request signing and webhook verification.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages