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 higher
  • httpx — installed automatically as a dependency

Installation

pip install tangentopay

Quick start

1. Accept a customer payment (storefront)

Use ServiceClient with your public service key (pk_live_...). Get it from: TangentoPay Dashboard → Services → your service → API Keys.

import tangentopay

client = tangentopay.ServiceClient("pk_live_your_service_key")

# Product-based checkout (e-commerce / WooCommerce)
session = client.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",
    cancel_url="https://myshop.com/cart",
)

# Redirect your customer to the hosted checkout page
redirect(session.redirect_url)

Amount-only checkout (payfac / money transfer)

Use when you have no product catalogue — payfac integrations, wallet top-ups via service key, or any scenario where you need to collect a fixed amount without listing items.

session = client.checkout.create(
    amount=5000,                   # total amount — no products list needed
    description="Account top-up", # shown on Stripe checkout page
    currency_code="XAF",
    return_url="https://myapp.com/success",
    cancel_url="https://myapp.com/cancel",
)

redirect(session.redirect_url)

2. Confirm payment before fulfilling an order

# On your /thank-you page the URL contains ?session_id=...
# Poll until the payment is confirmed (up to 60 seconds)

status = client.checkout.wait_for_completion(transaction_uid, timeout=60)
if status.is_completed:
    fulfill_order()

3. Manage payments on the backend (merchant)

Use MerchantClient with your API token — keep this server-side only, never expose it in a browser.

import os
import tangentopay

merchant = tangentopay.MerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])

# List recent payments
page = merchant.payments.list(per_page=20)
for txn in page.data:
    print(txn.transaction_uid, txn.transaction_status, txn.final_amount)

# Issue a refund
refund = merchant.refunds.create(
    transaction_uid="TXN-ABC123",
    amount=49.99,
    reason="Customer request",
    pin="1234",
    recipient_type="stripe",
)

# Check wallet balance
balance = merchant.wallets.main_balance()
print(balance.available_balance, balance.currency_code)

4. Verify incoming webhooks

Always verify the HMAC signature before trusting any webhook payload.

import os
import tangentopay

WEBHOOK_SECRET = os.environ["TANGENTOPAY_WEBHOOK_SECRET"]

def handle_webhook(raw_body: bytes, signature_header: str):
    try:
        event = tangentopay.Webhook.construct_event(
            payload=raw_body,
            signature=signature_header,
            secret=WEBHOOK_SECRET,
        )
    except tangentopay.WebhookSignatureError:
        return 400  # reject tampered or replayed events

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

    return 200

Authentication

TangentoPay uses two separate credentials depending on what you are doing:

Client Credential Header sent When to use
ServiceClient Service key (pk_live_...) X-Service-Key Creating checkout sessions, checking payment status — storefront / frontend server
MerchantClient API token (Bearer) Authorization: Bearer Everything sensitive — payments, refunds, payouts, wallets, analytics — backend only

Getting your credentials

  1. Log in to the TangentoPay Dashboard
  2. Go to Services and open your service
  3. Click API Keys
  4. Copy the Service Key and API Token
  5. Store them as environment variables — never commit them to git
# .env (never commit this file)
TANGENTOPAY_SERVICE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
TANGENTOPAY_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6...
TANGENTOPAY_WEBHOOK_SECRET=whs_live_xxxxxxxxxxxxxxxxxxxxxxxx  # from API Keys, not Webhook settings

Obtaining a token programmatically

# Step 1 — submit credentials (triggers OTP to your registered device)
token = tangentopay.login(
    email="me@example.com",
    password="secret",
    otp="123456",
)

merchant = tangentopay.MerchantClient(api_token=token)

The token does not expire automatically. Call merchant.auth.logout() to revoke it.


Resources

ServiceClient resources

Resource Methods Description
checkout create(), get_status(), wait_for_completion() Hosted Stripe checkout sessions

MerchantClient resources

Resource Methods Description
auth login(), verify_otp(), me(), logout(), change_password() Authentication and profile
payments list(), get(), create_manual() View and record payments
refunds create(), list() Issue and list refunds
topups create(), list() Add funds to a wallet
payouts create(), bulk(), list() Send funds to recipients
transfers to_main(), list() Move funds between wallets
wallets main_balance(), service_balance(), manual_balance() Check balances
services list(), get(), create(), update(), delete(), create_api_key(), rotate_api_key(), list_api_keys(), revoke_api_key(), update_webhook(), list_payment_methods(), set_payment_method(), set_payment_methods() Manage services, keys, and payment methods
customers list(), get(), create(), update(), delete(), import_csv() Customer management
analytics dashboard(), payments_chart(), gross_volume(), total_payouts() Reporting and analytics

Service setup

WordPress / WooCommerce plugin

  1. Log in to TangentoPay Dashboard
  2. Services → Create service — type: plugin
  3. API Keys → Create key — type: live (and test for test mode)
  4. Copy all three credentials immediately (shown once only):
    • public_key (pk_live_…) → Live Service Key in WooCommerce plugin settings
    • webhook_secret (whs_live_…) → Live Webhook Secret in WooCommerce plugin settings
  5. Copy the Webhook URL shown in WooCommerce → paste it into Dashboard → Webhooks
  6. The secret_key (sk_live_…) is not needed for the WordPress plugin — store it safely

SDK / server-side integration

import tangentopay

merchant = tangentopay.login("me@example.com", "password", "123456")

# Create key pair (run once during setup)
pair = merchant.services.create_api_key(
    service_id,
    key_name="Production server",
    key_type="live",
)
# Store immediately — shown once:
print(pair.public_key)      # pk_live_…  → X-Service-Key
print(pair.secret_key)      # sk_live_…
print(pair.webhook_secret)  # whs_live_… → webhook verification

# Rotate when needed (old credentials stop working immediately)
rotated = merchant.services.rotate_api_key(service_id, pair.id)
print(rotated.public_key)
print(rotated.webhook_secret)

Environment variables:

TANGENTOPAY_SERVICE_KEY=pk_live_…        # X-Service-Key for checkout
TANGENTOPAY_SECRET_KEY=sk_live_…         # privileged server calls (future)
TANGENTOPAY_WEBHOOK_SECRET=whs_live_…    # from API Keys, not Webhook settings
TANGENTOPAY_TEST_SERVICE_KEY=pk_test_…
TANGENTOPAY_TEST_WEBHOOK_SECRET=whs_test_…

Wallet top-up

Top-up lets authenticated merchants add funds to their TangentoPay wallet via card (Stripe) or Mobile Money (Fapshi). It uses the MerchantClient.

Card top-up (Stripe Checkout)

Why idempotency_key is required

Every call to topups.create() is an independent Python function call. If you retry after a network failure without passing the same key, the server sees a completely new request and creates a second Stripe Checkout Session — potentially charging the user twice.

The rule is simple: generate the key once, store it, reuse it on every retry of the same top-up intent.

import tangentopay

merchant = tangentopay.MerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])

# Step 1 — generate ONCE and store in your session / database
key = tangentopay.generate_idempotency_key()

# Step 2 — initiate the top-up (safe to retry with the same key)
session = merchant.topups.create(
    amount=50.00,
    currency_code="USD",
    idempotency_key=key,          # required
    return_url="https://app.com/topup/success",
    cancel_url="https://app.com/topup/cancel",
)

# Step 3 — redirect the user to complete payment
redirect(session.redirect_url)

On retry (network timeout, double-tap):

# Same key → server returns the existing session, no new charge
session = merchant.topups.create(
    amount=50.00,
    currency_code="USD",
    idempotency_key=key,   # same key as before
    return_url="https://app.com/topup/success",
)
# session.redirect_url is the same Stripe URL — user continues where they left off

Async variant:

session = await async_merchant.topups.create(
    amount=50.00,
    currency_code="USD",
    idempotency_key=key,
    return_url="https://app.com/topup/success",
)

Mobile Money top-up (MTN MoMo / Orange Money — XAF only)

Fapshi sends a USSD push to the subscriber's handset. The amount is debited immediately and credited to your wallet after confirmation.

Fee note: Fapshi adds a 2.2% processing fee on top of your requested amount. The gross_amount on the returned transaction shows the total charged to the subscriber's phone.

key = tangentopay.generate_idempotency_key()

session = merchant.topups.create(
    amount=10_000,            # XAF — amount you want credited to your wallet
    currency_code="XAF",
    idempotency_key=key,
    payment_source="mtn_momo",
    phone="677000000",        # Cameroon format: 6XXXXXXXX — no +237 prefix
)
# A USSD push is sent to 677000000 — poll status to confirm

For Orange Money, use payment_source="orange_money" with the same phone format.

Check the transaction outcome:

import time

for _ in range(12):          # poll for up to ~60 s
    status = merchant.topups.list(per_page=1).data[0]
    if status.is_completed:
        print(f"Credited {status.final_amount} {status.currency_code}")
        break
    if status.is_failed:
        print("Top-up failed or was cancelled by subscriber")
        break
    time.sleep(5)

Top-up without products

Unlike checkout sessions, top-ups do not require a products array. Pass amount + currency_code directly — the payment line item is created automatically.


Payment methods

Each service has its own set of enabled payment methods. Cards (Visa/Mastercard) are always enabled. Company accounts that have completed KYB verification can enable additional Stripe methods (Google Pay, Apple Pay, Alipay, WeChat Pay) per service via the SDK.

Method Default Availability
Visa / Mastercard / Amex (card) ✅ Always enabled All account types
Google Pay Off Company accounts with KYB verification
Apple Pay Off Company accounts with KYB verification
Alipay Off Company accounts with KYB verification
WeChat Pay Off Company accounts with KYB verification
MTN MoMo (mtn_momo) ✅ Available via Fapshi XAF (Cameroon) — wallet top-ups
Orange Money (orange_money) ✅ Available via Fapshi XAF (Cameroon) — wallet top-ups

Managing payment methods per service

# List all payment methods for a service (with enabled/locked/reason status)
methods = merchant.services.list_payment_methods(service_id)
# [ServicePaymentMethod(slug='card', enabled=True, locked=False), ...]

# Toggle a single method on or off
merchant.services.set_payment_method(service_id, 'google_pay', enabled=True)

# Replace the entire set of enabled methods at once
# card must always be included
merchant.services.set_payment_methods(service_id, ['card', 'apple_pay', 'alipay'])

Checkout sessions for that service will only show the methods you have enabled. If the account is not KYB-verified, non-card methods are returned as locked=True with a human-readable reason.

MoMo top-ups use payment_source="mtn_momo" or "orange_money" with a phone="6XXXXXXXX" parameter. See Wallet top-up for the full example.


Service request logs

Every API request authenticated via X-Service-Key is automatically logged. The logs mirror the Dashboard → Service → Logs view.

# List recent logs for a service (last 25 by default)
page = merchant.logs.list(
    service_id,
    per_page=25,
    status=500,        # filter by HTTP status code
    method="POST",     # filter by HTTP method
    date_from="2026-05-01",
    date_to="2026-05-31",
)

for entry in page["data"]["data"]:
    print(entry["request_id"], entry["status_code"], entry["application"], f"{entry['duration_ms']}ms")
# a1b2c3d4-...  200  WooCommerce Inc.  342ms

# Fetch a single entry by its request ID
# (same UUID as the X-Request-ID response header)
detail = merchant.logs.get(service_id, "a1b2c3d4-e5f6-7890-abcd-ef1234567890")
print(detail["data"]["transaction_uid"])  # TXN-...

Async version:

page = await merchant.logs.list(service_id, per_page=25)

Async support

Every client has an async counterpart — AsyncServiceClient and AsyncMerchantClient — with identical methods that return awaitables. Use these with FastAPI, Starlette, or any asyncio-based framework.

import asyncio
import tangentopay

async def main():
    client = tangentopay.AsyncServiceClient("pk_live_your_service_key")

    session = await client.checkout.create(
        products=[{"name": "Pro Plan", "price": 49.99, "quantity": 1}],
        currency_code="USD",
        return_url="https://myshop.com/thank-you",
        cancel_url="https://myshop.com/cart",
    )
    print(session.redirect_url)

asyncio.run(main())
# FastAPI example
from fastapi import FastAPI, Request
import tangentopay

app = FastAPI()
merchant = tangentopay.AsyncMerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])

@app.get("/payments")
async def list_payments():
    page = await merchant.payments.list(per_page=20)
    return {"total": page.total, "data": [p.transaction_uid for p in page.data]}

Error handling

All SDK errors inherit from tangentopay.TangentoPayError so you can catch everything with one clause or be specific.

try:
    refund = merchant.refunds.create(
        transaction_uid="TXN-001",
        amount=9999.00,
        reason="test",
        pin="wrong",
        recipient_type="stripe",
    )
except tangentopay.ValidationError as e:
    # Server-side field validation failed
    print(e.errors)           # {"amount": ["exceeds original transaction amount"]}
except tangentopay.AuthenticationError:
    # Token is invalid or expired — re-authenticate
    print("Invalid or expired token")
except tangentopay.PermissionError:
    # Authenticated but not allowed to perform this action
    print("Insufficient permissions")
except tangentopay.NotFoundError:
    print("Transaction not found")
except tangentopay.RateLimitError as e:
    # SDK already retried with exponential backoff and gave up
    print(f"Rate limited — retry after {e.retry_after}s")
except tangentopay.ServerError:
    # 5xx — SDK retried 3 times automatically before raising
    print("TangentoPay server error")
except tangentopay.NetworkError:
    # Timeout, DNS failure, connection refused
    print("Network error — check your connection")
except tangentopay.TangentoPayError as e:
    # Catch-all for any other SDK error
    print(f"Error {e.http_status}: {e.message}")

Exception reference

Exception HTTP status Notes
AuthenticationError 401 Invalid or expired API key / token
PermissionError 403 Authenticated but not authorised
NotFoundError 404 Resource does not exist
ValidationError 422 Field-level errors in e.errors dict
RateLimitError 429 After all retries exhausted; e.retry_after seconds
ServerError 5xx After 3 automatic retries
NetworkError Timeout, DNS, connection error
WebhookSignatureError Invalid HMAC, tampered payload, or replay attack

Webhook verification

TangentoPay signs every webhook with HMAC-SHA256 and includes a timestamp to prevent replay attacks. The SDK verifies both automatically.

from tangentopay.webhook import Webhook
import tangentopay

event = Webhook.construct_event(
    payload=raw_body,           # bytes or str — the raw request body
    signature=sig_header,       # value of the X-TangentoPay-Signature header
    secret=webhook_secret,      # whs_live_... or whs_test_... from API Keys (shown once)
    timestamp_tolerance_seconds=300,  # default — reject events older than 5 minutes
)

Signature header format:

X-TangentoPay-Signature: t=1716134400,sha256=abcdef1234...

Django example:

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from tangentopay.webhook import Webhook
import tangentopay

@csrf_exempt
def webhook(request):
    try:
        event = Webhook.construct_event(
            payload=request.body,
            signature=request.headers.get("X-TangentoPay-Signature", ""),
            secret=settings.TANGENTOPAY_WEBHOOK_SECRET,
        )
    except tangentopay.WebhookSignatureError as e:
        return HttpResponse(str(e), status=400)

    match event.event:
        case "transaction.payment_completed":
            handle_payment(event.payload["transaction_uid"])
        case "transaction.refund_completed":
            handle_refund(event.payload["transaction_uid"])

    return HttpResponse(status=200)

Flask example:

from flask import Flask, request, abort
from tangentopay.webhook import Webhook
import tangentopay

app = Flask(__name__)

@app.post("/webhooks/tangentopay")
def webhook():
    try:
        event = Webhook.construct_event(
            payload=request.data,
            signature=request.headers.get("X-TangentoPay-Signature", ""),
            secret=app.config["TANGENTOPAY_WEBHOOK_SECRET"],
        )
    except tangentopay.WebhookSignatureError:
        abort(400)

    if event.event == "transaction.payment_completed":
        handle_payment(event.payload["transaction_uid"])

    return "", 200

Supported webhook events

Event When it fires
transaction.payment_completed Payment successfully processed
transaction.payment_failed Payment attempt failed
transaction.refund_completed Refund issued successfully
transaction.payout_completed Payout sent to recipient
transaction.topup_completed Wallet top-up completed

Supported currencies

TangentoPay supports Stripe's full currency list. Commonly used currencies:

Code Currency
USD US Dollar
EUR Euro
GBP British Pound
XAF Central African CFA Franc (Cameroon, Chad, Congo, Gabon…)
NGN Nigerian Naira
GHS Ghanaian Cedi
KES Kenyan Shilling
ZAR South African Rand

XAF note: Amounts in XAF are zero-decimal — pass 500 not 5.00. The SDK handles this automatically when you provide currency_code="XAF".


Contributing

See CONTRIBUTING.md for development setup, branch naming, commit conventions, code style, and release instructions.


Security

Security issues should not be reported via public GitHub issues.

Please report vulnerabilities by emailing security@tangentopay.com. We will acknowledge within 48 hours and aim to release a fix within 7 days for critical issues.

See SECURITY.md for the full security policy.

Security features built into this SDK

  • HTTPS enforced — the SDK rejects any base_url that does not use https://, preventing accidental credential leakage over plain HTTP
  • Header injection protection — credentials are validated for CR/LF characters at construction time, preventing HTTP header injection attacks
  • Webhook replay protectionconstruct_event() rejects events with timestamps outside a configurable tolerance window (default 5 minutes)
  • Webhook hex validation — the SHA-256 digest in the signature header is validated as exactly 64 hex characters before comparison
  • Timing-safe comparison — webhook signatures are verified with hmac.compare_digest() to prevent timing side-channel attacks
  • Payload size limit — webhook payloads over 10 MB are rejected before any HMAC computation
  • Credential masking — API keys and tokens are masked in repr() output so they do not appear in logs or debug output
  • Capped retry backoff — the Retry-After value from the server is capped at 60 seconds to prevent server-controlled denial-of-service
  • Protected auth headersextra_headers cannot override Authorization or X-Service-Key

License

MIT — see LICENSE for the full text.


Built with ❤️ by the TangentoPay team

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.4.0.tar.gz (46.0 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.4.0-py3-none-any.whl (45.9 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for tangentopay-0.4.0.tar.gz
Algorithm Hash digest
SHA256 8036cec4530fad0a9c181c13958088d88eac31785ae28690849efde752f314a7
MD5 a1e4e5c618104ae9a1cf0e7960ce039b
BLAKE2b-256 574716669bb98d30bef26acaef7c4897e4c85d92aac45e70580ee5f0dd2133a7

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tangentopay-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 45.9 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.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f96332ab79b4ef0483c5c6e8e4f8ee572d10b22127ad45485d90f784e1117bba
MD5 90959ef676ca02febb0c37e9fd409fdd
BLAKE2b-256 c4d0a05d28f80d6cec0608f6d564f9b821a7cadbc98947e123076073b087e3ea

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