Skip to main content

Official Python SDK for the TangentoPay API

Project description

tangentopay-python

Official Python SDK for the TangentoPay API — accept payments, issue refunds, manage wallets, and verify webhooks with a clean, type-safe interface.

PyPI version CI Python 3.9+ License: MIT


Table of contents


Requirements

  • Python 3.9 or later
  • A TangentoPay account — sign up

Installation

pip install tangentopay

# with async support (HTTP/2 via httpx)
pip install "tangentopay[async]"

Quick start

import tangentopay

# ── Storefront: create a Stripe checkout session ──────────────────────────────
service = tangentopay.ServiceClient(service_key="pk_live_<your_service_key>")

session = service.checkout.create(
    products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
    currency_code="USD",
    customer_email="buyer@example.com",
    return_url="https://myshop.com/thank-you",
)
# redirect your customer to session.redirect_url

# ── Backend: manage payments with an API token ────────────────────────────────
merchant = tangentopay.MerchantClient(api_token="<your_bearer_token>")

payments = merchant.payments.list(per_page=20)
balance  = merchant.wallets.main_balance()

Authentication

TangentoPay has two credential types:

Credential Where it goes Client to use
Service Key (pk_live_… / pk_test_…) X-Service-Key header ServiceClient
API Token (Bearer) Authorization: Bearer … MerchantClient

Never expose an API token in browser or mobile code. Use ServiceClient on the frontend and MerchantClient only on your server.

ServiceClient

service = tangentopay.ServiceClient(
    service_key="pk_live_<your_service_key>",
    # optional:
    base_url="https://api.tangentopay.com/api/v1",
    timeout=30.0,
    max_retries=3,
)

MerchantClient

merchant = tangentopay.MerchantClient(api_token="<your_bearer_token>")

Obtain an API token programmatically:

client = tangentopay.MerchantClient()
client.auth.login(email=email, password=password)
token = client.auth.verify_otp(email=email, otp=otp)
merchant = tangentopay.MerchantClient(api_token=token.access_token)

Token expiry and refresh

Catch AuthenticationError and re-authenticate in place:

from tangentopay import AuthenticationError

try:
    payments = merchant.payments.list()
except AuthenticationError:
    merchant.auth.login(email=email, password=password)
    token = merchant.auth.verify_otp(email=email, otp=otp)
    merchant.set_token(token.access_token)  # updates all resources in place
    payments = merchant.payments.list()     # retry

Resources

ServiceClient resources

Attribute Description
service.checkout Create Stripe-hosted checkout sessions; poll payment status
service.topups Collect money from a customer's MoMo account into the service wallet
service.withdrawals Send money from the service wallet to a customer's MoMo account
service.provider_status Real-time health for MTN MoMo, Orange Money, and Stripe

MerchantClient resources

Attribute Description
merchant.auth Login, OTP verification, profile, logout
merchant.payments View and search your incoming payment history
merchant.refunds Issue refunds on completed payments
merchant.topups Top up your main wallet via card or MoMo
merchant.payouts Send funds out (bank, MoMo, TP wallet, debit card)
merchant.wallets Main and service wallet balances
merchant.services View services; manage enabled payment methods per service
merchant.customers Create and manage customer records
merchant.analytics Payment summaries, revenue, and volume over time
merchant.logs Per-service API request logs
merchant.transfers Internal wallet transfer history
merchant.provider_status Real-time health for MTN MoMo, Orange Money, and Stripe

Note on service administration: Creating services, rotating API keys, updating webhooks, and other one-time setup tasks are done from the TangentoPay Dashboard. These operations are intentionally not exposed in the SDK.


Provider status

Check provider health before initiating any collection or disbursement — this lets you show users a clear error message instead of a silent payment failure.

status = merchant.provider_status.get()
# or: service.provider_status.get()

# status is a dict keyed by provider slug:
# {
#   "mtn_momo":     ProviderStatusEntry(slug="mtn_momo",     name="MTN Mobile Money", status="operational", ...),
#   "orange_money": ProviderStatusEntry(slug="orange_money", name="Orange Money",      status="degraded",    ...),
#   "stripe":       ProviderStatusEntry(slug="stripe",       name="Stripe",            status="operational", ...),
# }

if status["mtn_momo"].status == "down":
    raise PaymentUnavailableError(
        "MTN Mobile Money is currently unavailable. Try Orange Money or pay by card."
    )

Possible status values:

Value Meaning
"operational" Fully functional — proceed normally
"degraded" Partial outage — expect higher failure rates
"down" Provider unreachable — do not attempt payments

Currency and provider guide

Provider Supported currencies Notes
MTN Mobile Money XAF only Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF.
Orange Money XAF only Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF.
Stripe USD, EUR, GBP, and more Multi-currency card checkout and instant payouts.

When a customer pays via MoMo the transaction currency is XAF. When they pay via Stripe card the currency is whatever currency_code you pass to checkout.create().

Use merchant.wallets.main_balance() to get per-currency balances — the response includes a balances list showing only currencies with a non-zero funded amount, which you can use to build a currency-selector UI in your withdrawal flow.


Service wallet operations (B2B2C)

The service wallet is funded when customers pay through your service's checkout flow.

# ── Collect from a customer's MoMo ───────────────────────────────────────────
topup = service.topups.create(
    amount=5000,               # XAF
    customer_phone="237XXXXXXXXX",
    external_ref="ORDER-001",
    notify_url="https://yourapp.com/webhooks/momo",
)
# pending — wallet credited after Fapshi webhook confirms

# ── Disburse to a customer's MoMo ────────────────────────────────────────────
withdrawal = service.withdrawals.create(
    amount=4000,               # XAF
    recipient_phone="237XXXXXXXXX",
    external_ref="PAYOUT-001",
)

Payouts

Two-step flow: initiate → confirm.

# Step 1 — initiate
initiation = merchant.payouts.initiate(
    amount=50_000,
    currency_code="XAF",
    recipient_type="tangentopay_wallet",
    recipient_details={"wallet_address": "user@example.com"},
    note="Freelance payment",
)

# Step 2 — confirm with payout PIN
merchant.payouts.confirm(initiation.payout_ref, pin=os.environ["PAYOUT_PIN"])

Virtual card payout (USD, Instant Payout)

# Option A: use a saved card
merchant.payouts.initiate(
    amount=100,
    currency_code="USD",
    recipient_type="virtual_card",
    recipient_details={"payout_method_id": "pm_..."},
)

# Option B: one-time Stripe.js token (card never stored)
merchant.payouts.initiate(
    amount=100,
    currency_code="USD",
    recipient_type="virtual_card",
    recipient_details={"stripe_token_id": "tok_..."},
)

Bulk payout

with open("payouts.csv", "rb") as f:
    batch = merchant.payouts.bulk.initiate(
        csv_file=f,
        default_recipient_type="tangentopay_wallet",
    )
merchant.payouts.bulk.confirm(batch.batch_ref, pin=os.environ["PAYOUT_PIN"])

Merchant wallet top-up

# Via MoMo
topup = merchant.topups.create(
    amount=100_000,        # XAF
    phone="237XXXXXXXXX",
    provider="mtn_momo",
)

# Via card (Stripe-hosted page)
card_topup = merchant.topups.create_card_topup(
    amount=200,
    currency_code="USD",
    return_url="https://dashboard.yourapp.com/wallet",
)

Payment methods

methods = merchant.services.list_payment_methods(service_id)
# [{"slug": "mtn_momo", "name": "MTN Mobile Money", "enabled": True, "locked": False, ...}, ...]

# Disable Orange Money if provider is down
status = merchant.provider_status.get()
if status["orange_money"].status == "down":
    merchant.services.set_payment_method(service_id, "orange_money", enabled=False)

# Replace entire set (card must always be included)
merchant.services.set_payment_methods(service_id, ["card", "mtn_momo"])

Async support

Every resource has an async equivalent — use AsyncServiceClient and AsyncMerchantClient:

import asyncio
import tangentopay

async def main():
    service  = tangentopay.AsyncServiceClient(service_key="pk_live_<your_service_key>")
    merchant = tangentopay.AsyncMerchantClient(api_token="<your_bearer_token>")

    status   = await merchant.provider_status.get()
    payments = await merchant.payments.list()
    session  = await service.checkout.create(
        products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
        currency_code="USD",
        customer_email="buyer@example.com",
        return_url="https://myshop.com/thank-you",
    )

asyncio.run(main())

Error handling

from tangentopay import (
    AuthenticationError,   # 401
    PermissionError,       # 403
    NotFoundError,         # 404
    ValidationError,       # 422 — includes .errors dict
    RateLimitError,        # 429 — includes .retry_after seconds
    ServerError,           # 5xx
    NetworkError,          # connection-level failure
    TangentoPayError,      # base class for all above
)

try:
    merchant.payouts.initiate(...)
except ValidationError as e:
    print(e.errors)          # field-level validation messages
except RateLimitError as e:
    print(f"Retry after {e.retry_after}s")
except TangentoPayError as e:
    raise

Webhook verification

from tangentopay import Webhook, WebhookSignatureError

@app.route("/webhooks/tangentopay", methods=["POST"])
def handle_webhook():
    try:
        event = Webhook.construct_event(
            payload=request.data,
            signature=request.headers["X-TangentoPay-Signature"],
            secret=os.environ["TANGENTOPAY_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return "Bad signature", 400

    if event.event == "transaction.payment_completed":
        fulfill_order(event.payload)

    return {"received": True}

Security

Please report security vulnerabilities to security@tangentopay.com rather than opening a public issue.


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

tangentopay-0.8.0.tar.gz (45.3 kB view details)

Uploaded Source

Built Distribution

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

tangentopay-0.8.0-py3-none-any.whl (47.2 kB view details)

Uploaded Python 3

File details

Details for the file tangentopay-0.8.0.tar.gz.

File metadata

  • Download URL: tangentopay-0.8.0.tar.gz
  • Upload date:
  • Size: 45.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for tangentopay-0.8.0.tar.gz
Algorithm Hash digest
SHA256 46c00dfb1551ed9f184478e4104258dfe82514a91b7d2699a97f803d0e1bd225
MD5 585604530ffcac8182880984d25cb212
BLAKE2b-256 c5e287eead956d9f4e92777464338616444cfceee84dd3f341c38cd09d060ce8

See more details on using hashes here.

File details

Details for the file tangentopay-0.8.0-py3-none-any.whl.

File metadata

  • Download URL: tangentopay-0.8.0-py3-none-any.whl
  • Upload date:
  • Size: 47.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for tangentopay-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd576ef2f2728e99ff5c098f668c6ae4fe23e5c1b8684f40cd83911673701f61
MD5 447505899218ddce0cf55efa06b43bc9
BLAKE2b-256 e9cb64848c1db6924be7ae13e4a653cae4181aee4c34369e909cf6156f1d587e

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