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.
Table of contents
- Requirements
- Installation
- Quick start
- Authentication
- Resources
- Wallet top-up
- Payment methods
- Async support
- Error handling
- Webhook verification
- Supported currencies
- Security
- License
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")
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)
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
- Log in to the TangentoPay Dashboard
- Go to Services and open your service
- Click API Keys
- Copy the Service Key and API Token
- 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=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
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(), list_api_keys(), revoke_api_key(), update_webhook() |
Manage services and their keys |
customers |
list(), get(), create(), update(), delete(), import_csv() |
Customer management |
analytics |
dashboard(), payments_chart(), gross_volume(), total_payouts() |
Reporting and analytics |
Wallet top-up
Top-up lets authenticated merchants add funds to their TangentoPay wallet via Stripe Checkout. It uses the MerchantClient.
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",
)
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
Payment methods available on the Stripe Checkout page are controlled by your Stripe Dashboard settings — TangentoPay does not hard-code them.
| Method | Default | Enable via |
|---|---|---|
| Visa / Mastercard / Amex (card) | ✅ On for all accounts | Always available |
| Google Pay | Off | Stripe Dashboard → Payment methods |
| Apple Pay | Off | Stripe Dashboard → Payment methods |
| Alipay | Off | Stripe Dashboard → Payment methods |
| WeChat Pay | Off | Stripe Dashboard → Payment methods |
| MoMo (Mobile Money) | Coming soon | Will be added as a native TangentoPay method |
Cards are accepted by default for every account type. Wallets (Google Pay, Apple Pay) and regional methods (Alipay, WeChat Pay) can be toggled on or off per account from the Stripe Dashboard without any code changes.
MoMo note: Mobile Money support is on the roadmap and will be integrated as a first-class TangentoPay payment method, separate from Stripe.
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, # whsec_... from your dashboard
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
500not5.00. The SDK handles this automatically when you providecurrency_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_urlthat does not usehttps://, 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 protection —
construct_event()rejects events with timestamps outside a configurable tolerance window (default 5 minutes) - 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-Aftervalue from the server is capped at 60 seconds to prevent server-controlled denial-of-service - Protected auth headers —
extra_headerscannot overrideAuthorizationorX-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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tangentopay-0.1.5.tar.gz.
File metadata
- Download URL: tangentopay-0.1.5.tar.gz
- Upload date:
- Size: 38.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f85f69a1d7b44389cf469513ccef7e0f57f3fb4d05e188bf94f32ce4f1081826
|
|
| MD5 |
b915b3554ca6d092371c0ecd442d3b7b
|
|
| BLAKE2b-256 |
dfd188f3d5ed0cd857fe68a73de8e0ab019cd6a69e78be5474d0e753b8bb925e
|
File details
Details for the file tangentopay-0.1.5-py3-none-any.whl.
File metadata
- Download URL: tangentopay-0.1.5-py3-none-any.whl
- Upload date:
- Size: 38.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ae3160d9a48ed49bc7576b4ab9f5327a59b99255db55be51d30e683e23e5996
|
|
| MD5 |
27cc68e7fa9520389a3d347217baaa48
|
|
| BLAKE2b-256 |
40e130847f0df04738505d01b0ce6cf62ff698cab3e9ea59d4f3fd7920a05e16
|