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
- Token expiry and refresh
- Resources
- Service wallet operations (B2B2C)
- Payouts
- Merchant 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")
# 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 or any scenario where you need to collect a fixed amount without listing items via Stripe checkout. For collecting money directly into a service wallet via MoMo USSD, use service.topups.create() instead.
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 |
Checkout sessions, service wallet top-ups & withdrawals — backend server |
MerchantClient |
API token (Bearer) | Authorization: Bearer |
Everything sensitive — payments, refunds, payouts, wallets, analytics — backend only, never mobile |
Understanding the three API key credentials
When you create a key pair (Dashboard → API Keys → Create key), you receive three separate values:
| Credential | Format | What it's for |
|---|---|---|
public_key |
pk_live_... / pk_test_... |
Pass as service_key to ServiceClient. Safe for backend servers. |
secret_key |
sk_live_... / sk_test_... |
Never put this in a mobile app or browser. Server-side only. Reserved for future privileged API routes — no SDK method currently requires it. Store it in a secrets manager. |
webhook_secret |
whs_live_... / whs_test_... |
Paste into your webhook handler to verify HMAC-SHA256 signatures. Never expose in client code. |
Getting your credentials
- Log in to the TangentoPay Dashboard
- Go to Services and open your service
- Click API Keys → Create key — store all three values immediately, they are shown once only
- Store them as environment variables — never commit them to git
# .env (never commit this file)
TANGENTOPAY_SERVICE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxx # → ServiceClient
TANGENTOPAY_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx # → reserved, store safely
TANGENTOPAY_WEBHOOK_SECRET=whs_live_xxxxxxxxxxxxxxxxxxxxxxxx # → webhook verification
TANGENTOPAY_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6... # → MerchantClient (Bearer token)
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 Bearer token expires. When it does, API calls raise
AuthenticationError(HTTP 401). See Token expiry and refresh below.
Token expiry and refresh
The Bearer token obtained from auth.verify_otp() expires. When it does, the next call raises AuthenticationError.
Use set_token() to swap in a fresh token without rebuilding the client:
import tangentopay
merchant = tangentopay.MerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])
def call_with_refresh(fn):
try:
return fn()
except tangentopay.AuthenticationError:
# Re-authenticate and update the token in-place
new_token = tangentopay.login(
email=os.environ["TANGENTOPAY_EMAIL"],
password=os.environ["TANGENTOPAY_PASSWORD"],
otp=get_otp(), # retrieve from your TOTP app or email
)
merchant.set_token(new_token) # all resources use the new token automatically
return fn() # retry
# Usage — transparent to callers
page = call_with_refresh(lambda: merchant.payments.list(per_page=20))
Tip: Store the token in your database or cache (Redis), and check its expiry before each request rather than waiting for a 401.
Resources
ServiceClient resources
| Resource | Methods | Description |
|---|---|---|
checkout |
create(), get_status(), wait_for_completion() |
Hosted Stripe checkout sessions |
topups |
create() |
Collect money from a customer's MoMo into the service wallet via Fapshi USSD push |
withdrawals |
create() |
Debit the service wallet and pay a customer's MoMo account via Fapshi disbursement |
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
- Log in to TangentoPay Dashboard
- Services → Create service — type:
plugin - API Keys → Create key — type:
live(andtestfor test mode) - Copy all three credentials immediately (shown once only):
public_key(pk_live_…) → Live Service Key in WooCommerce plugin settingswebhook_secret(whs_live_…) → Live Webhook Secret in WooCommerce plugin settings
- Copy the Webhook URL shown in WooCommerce → paste it into Dashboard → Webhooks
- The
secret_key(sk_live_…) is not needed for the WordPress plugin — store it safely
SDK / server-side / mobile integration
Which service type should I choose? The service_type field is a dashboard label only — it has no effect on which API endpoints or SDK methods are available:
| Type | Use when |
|---|---|
mobile |
Native iOS or Android app |
sdk |
Integrating via a TangentoPay SDK (Python, Node, PHP) |
web |
Web frontend or server-rendered website |
plugin |
WordPress / WooCommerce plugin |
api |
Direct API calls / custom backend |
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 (ServiceClient)
print(pair.secret_key) # sk_live_… → reserved for future privileged routes — store safely, never expose
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 and service wallet ops
TANGENTOPAY_SECRET_KEY=sk_live_… # reserved — store securely, not in mobile apps
TANGENTOPAY_WEBHOOK_SECRET=whs_live_… # from API Keys, not Webhook settings
TANGENTOPAY_TEST_SERVICE_KEY=pk_test_…
TANGENTOPAY_TEST_WEBHOOK_SECRET=whs_test_…
Service wallet operations (B2B2C)
If you are building a B2B2C app (e.g. a "Jangi" wallet app where end-users hold balances inside your app), use ServiceClient to move money between your users' MoMo accounts and your service wallet. Your app tracks each user's individual balance in your own database; TangentoPay holds the aggregate service balance.
Wallet architecture:
- Main wallet — your merchant account balance. Receives merchant top-ups via
MerchantClient. Unrelated to end-user flows.- Service wallet — per-service balance. Receives customer MoMo collections (
service.topups.create()) and is debited for customer MoMo payouts (service.withdrawals.create()).
Collect from a customer (service top-up)
import tangentopay
service = tangentopay.ServiceClient(os.environ["TANGENTOPAY_SERVICE_KEY"])
# Customer "Alice" wants to load 5 000 XAF into her Jangi wallet
txn = service.topups.create(
amount=5000, # XAF — net amount credited to service wallet
phone="677001234", # Alice's MTN MoMo — format: 6XXXXXXXX (no country code)
idempotency_key="jangi-topup-alice-order-001",
)
print(txn["status"]) # "pending"
# ✅ Alice receives a USSD push on her phone.
# ✅ Once approved, Fapshi fires a webhook and TangentoPay credits your service wallet.
# ✅ Listen for transaction.completed webhook, then credit Alice's balance in your DB.
Fapshi fee on collections: 2.2% added on top — Alice pays ~5 112 XAF; your service wallet receives 5 000 XAF. Minimum: 100 XAF. Maximum: 500 000 XAF.
Pay out to a customer (service withdrawal)
# Customer "Bob" wants to cash out 10 000 XAF from his Jangi wallet
txn = service.withdrawals.create(
amount=10000, # XAF debited from service wallet
phone="690001234", # Bob's Orange Money number
idempotency_key="jangi-cashout-bob-req-42",
)
print(txn["status"]) # "processing"
print(txn["net_amount"]) # 9600 — Bob receives 96% after 4% TangentoPay fee
# ✅ Service wallet debited 10 000 XAF immediately.
# ✅ Fapshi sends Bob 9 600 XAF on his MoMo (Fapshi charges 0% on disbursements).
# ✅ On failure, the 10 000 XAF is automatically re-credited to your service wallet.
Minimum: 500 XAF. TangentoPay fee: 4% deducted from proceeds.
Checking service wallet balance
import tangentopay
merchant = tangentopay.MerchantClient(api_token=os.environ["TANGENTOPAY_API_TOKEN"])
# Service wallet — funded by customer MoMo collections
service_bal = merchant.wallets.service_balance()
print(service_bal) # [{'currency': 'XAF', 'available': 50000.0, 'incoming': 0.0}]
# Main wallet — funded by merchant top-ups via MerchantClient
main_bal = merchant.wallets.main_balance()
print(main_bal)
Why is my service wallet balance zero after a top-up? Merchant top-ups via
MerchantClient.topups.create()go to the main wallet, not the service wallet. UseServiceClient.topups.create()(customer MoMo collection) to fund the service wallet.
Payouts
Payouts let merchants withdraw funds from their TangentoPay wallet to a bank account, mobile money number, or another TangentoPay account. All payouts use MerchantClient.
Fee: TangentoPay charges a 4% fee deducted from proceeds.
transaction.net_amountshows what the recipient receives.
Bank payout (Stripe)
Requires a saved payout method (ba_... token linked via the Dashboard or API).
Processing time: 2–7 business days.
txn = merchant.payouts.create(
amount=50_000,
currency_code="XAF",
pin="1234",
recipient_type="bank",
recipient_details={
"bank_account_id": "ba_xxx", # from saved payout methods
"bank_name": "Ecobank",
"account_number": "123456789",
"first_name": "Jane",
"last_name": "Doe",
},
)
print(txn.net_amount) # amount after 4% fee
Mobile Money payout (MTN MoMo / Orange Money)
XAF only. Near-instant via Fapshi USSD push.
txn = merchant.payouts.create(
amount=10_000,
currency_code="XAF",
pin="1234",
recipient_type="mtn_momo", # or "orange_money"
recipient_details={
"mobile_money_number": "677000000", # 6XXXXXXXX — no country code
},
)
TangentoPay wallet transfer
Instant internal transfer to another TangentoPay account — works across all currencies.
txn = merchant.payouts.create(
amount=5_000,
currency_code="XAF",
pin="1234",
recipient_type="tangentopay_wallet",
recipient_details={
"wallet_tag": "JANE1234", # 4–32 uppercase alphanumeric chars
"account_name": "Jane Doe", # display name from tag lookup
},
)
print(txn.transaction_uid, txn.net_amount)
Looking up a wallet tag: Use the tag-lookup endpoint before initiating a transfer to pre-fill
account_nameand confirm the recipient exists.
Virtual card withdrawal (Stripe Instant Payout)
Withdraw to a Visa or Mastercard debit card saved via POST /payout-methods. Funds arrive in minutes via Stripe Instant Payout. USD only.
Prerequisites: The card must be saved first using the Stripe.js token flow (
method_type: "card",stripe_token_id: "tok_..."). See the API docs for the full card onboarding flow.
# Recommended — reference the saved card payout method by ID
txn = merchant.payouts.create(
amount=100.00,
currency_code="USD",
pin="1234",
recipient_type="virtual_card",
recipient_details={
"payout_method_id": 3, # ID from GET /payout-methods (method_type: "card")
},
)
print(txn.transaction_uid, txn.net_amount)
# Alternative — pass the Stripe card external account ID directly
txn = merchant.payouts.create(
amount=100.00,
currency_code="USD",
pin="1234",
recipient_type="virtual_card",
recipient_details={
"stripe_card_id": "card_1ObbCP2eZvKYlo2C5OlqLpOw", # card_... from POST /payout-methods
},
)
Note: TangentoPay charges a 4% fee. Stripe also charges a ~1% instant payout fee on top. The
net_amounton the returned transaction reflects both deductions.
Bulk payouts (payroll / commission)
results = merchant.payouts.bulk([
{
"amount": 150_000, "currency_code": "XAF", "pin": "1234",
"recipient_type": "mtn_momo",
"recipient_details": {"mobile_money_number": "677111111"},
},
{
"amount": 200_000, "currency_code": "XAF", "pin": "1234",
"recipient_type": "tangentopay_wallet",
"recipient_details": {"wallet_tag": "BOB9999", "account_name": "Bob Smith"},
},
])
Async variant
txn = await async_merchant.payouts.create(
amount=5_000,
currency_code="XAF",
pin="1234",
recipient_type="tangentopay_wallet",
recipient_details={"wallet_tag": "JANE1234", "account_name": "Jane Doe"},
)
Merchant wallet top-up
Top-up lets the merchant (you, the business owner) add funds to your TangentoPay main wallet via card (Stripe) or Mobile Money (Fapshi). It uses the MerchantClient.
⚠️ This is for merchant self-funding only. For collecting money from your app's end-users into a service wallet, see Service wallet operations above.
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_amounton 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 aphone="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 |
Customer payment (checkout) successfully processed |
transaction.payment_failed |
Customer payment attempt failed |
transaction.refund_completed |
Refund issued successfully |
transaction.payout_completed |
Merchant payout sent to recipient |
transaction.topup_completed |
Merchant main-wallet top-up completed |
transaction.completed |
Service wallet top-up or withdrawal confirmed by Fapshi |
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) - 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-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
Release history Release notifications | RSS feed
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.7.0.tar.gz.
File metadata
- Download URL: tangentopay-0.7.0.tar.gz
- Upload date:
- Size: 63.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e6fa19d2e52b5caedefdec27adcc65592e5bd4754fe27635f6427601d4aab56
|
|
| MD5 |
d42c13437510c66cbad0dea418c0c4d3
|
|
| BLAKE2b-256 |
ddc4c55c01945837e3e471c83d259a8fd7b9f5f0b5f3b528a572c371939c4c8d
|
File details
Details for the file tangentopay-0.7.0-py3-none-any.whl.
File metadata
- Download URL: tangentopay-0.7.0-py3-none-any.whl
- Upload date:
- Size: 54.3 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 |
197778cf835d3c31ecbc991036ff1700eff186552499b509a2c14d5f69427adb
|
|
| MD5 |
adc32e1d45f073d40d1463b7fdc84251
|
|
| BLAKE2b-256 |
ec04a75413d99f6c9c7aa375ea82fdbe5d429439be28ccfcb429ea1f7e924bfc
|