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")

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

  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=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

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 500 not 5.00. The SDK handles this automatically when you provide currency_code="XAF".


File structure

tangentopay-python/
│
├── tangentopay/                  # Main package
│   ├── __init__.py               # Public API — everything a user imports
│   ├── client.py                 # ServiceClient, MerchantClient, async variants
│   ├── _http.py                  # Internal HTTP transport (httpx wrapper, retries)
│   ├── models.py                 # Typed dataclasses for API responses
│   ├── exceptions.py             # All SDK exceptions + _raise_for_status()
│   ├── webhook.py                # Webhook signature verification
│   └── resources/                # One file per API resource
│       ├── __init__.py
│       ├── checkout.py           # ServiceClient: create sessions, poll status
│       ├── auth.py               # login, OTP, me, logout
│       ├── payments.py           # list, get, create_manual
│       ├── refunds.py            # create, list
│       ├── topups.py             # create, list
│       ├── payouts.py            # create, bulk, list
│       ├── transfers.py          # to_main, list
│       ├── wallets.py            # balances
│       ├── services.py           # service CRUD + API key management
│       ├── customers.py          # customer CRUD + CSV import
│       └── analytics.py          # dashboard stats and charts
│
├── tests/                        # pytest test suite
│   ├── conftest.py               # Shared fixtures (ServiceClient, MerchantClient)
│   ├── test_client.py            # Client instantiation and security guards
│   ├── test_checkout.py          # Checkout resource tests
│   ├── test_payments.py          # Payments resource tests
│   ├── test_exceptions.py        # Error mapping tests
│   └── test_webhook.py           # Webhook signature verification tests
│
├── examples/                     # Runnable usage examples
│   ├── checkout_example.py       # Full checkout flow
│   ├── merchant_example.py       # Payments, refunds, wallets
│   └── webhook_example.py        # Webhook handling with Flask
│
├── .github/
│   └── workflows/
│       ├── ci.yml                # Run tests on every push/PR (Python 3.9–3.12)
│       └── publish.yml           # Publish to PyPI on git tag v*.*.*
│
├── pyproject.toml                # Project metadata, dependencies, tool config
├── CHANGELOG.md                  # Version history
├── SECURITY.md                   # Security policy and vulnerability reporting
├── LICENSE                       # MIT
└── README.md                     # This file

Contributing

Contributions are welcome — bug fixes, new features, documentation improvements, and additional tests. Please follow the guidelines below so reviews go smoothly.

Branch naming

All branches must follow this naming convention:

Type Pattern Example
New feature feat/<short-description> feat/async-retry-logic
Bug fix fix/<short-description> fix/webhook-timestamp-parse
Documentation docs/<short-description> docs/async-examples
Refactor (no behaviour change) refactor/<short-description> refactor/http-client-cleanup
Tests test/<short-description> test/analytics-resource-coverage
CI / tooling ci/<short-description> ci/add-python-313-matrix
Chore / maintenance chore/<short-description> chore/bump-httpx-version
Hotfix (urgent production fix) hotfix/<short-description> hotfix/invalid-signature-crash
Release preparation release/<version> release/0.2.0

Rules:

  • All lowercase, words separated by hyphens — no underscores, no spaces
  • Keep the description short (2–4 words)
  • Branch off from main unless it's a hotfix on a release tag
  • Delete branches after they are merged

Workflow

  1. Fork the repository (external contributors) or create a branch directly (team members)
  2. Branch using the naming convention above
  3. Write tests — all new behaviour must be covered; bugfixes must include a regression test
  4. Run the full test suite locally before pushing (see Running tests)
  5. Open a pull request against main with a clear title and description
  6. Wait for CI — all checks must pass before merge
  7. Squash and merge is preferred for small changes; merge commits for larger features

Commit messages

Follow the Conventional Commits format:

<type>(<scope>): <short summary>

[optional body]
Type When to use
feat New feature or capability
fix Bug fix
docs Documentation only
test Adding or fixing tests
refactor Code change with no behaviour change
chore Dependency bumps, tooling, CI changes
security Security fix

Examples:

feat(webhook): add timestamp replay attack protection
fix(payments): include query params in list() mock URLs
docs(readme): add file structure and contributing guide
security(http): validate credentials for CR/LF injection at init time
chore: bump httpx upper bound to <2.0.0

Code style

The project uses Ruff for linting and formatting. Run before pushing:

ruff check tangentopay/
ruff format tangentopay/

Type annotations are required on all public functions and methods. Run mypy to check:

mypy tangentopay/

What to work on

Check the open issues for anything tagged good first issue or help wanted. If you want to add something that isn't listed, open an issue first to discuss it before writing code.


Development setup

# Clone the repository
git clone https://github.com/Grut-Design-Agency/tangentopay-python.git
cd tangentopay-python

# Create a virtual environment
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

# Install the package in editable mode with all dev dependencies
pip install -e ".[dev]"

Running tests

# Run the full suite
pytest

# Run with verbose output
pytest -v

# Run a single test file
pytest tests/test_webhook.py -v

# Run a single test
pytest tests/test_webhook.py::test_valid_signature -v

# Run with coverage report
pip install pytest-cov
pytest --cov=tangentopay --cov-report=term-missing

The test suite uses pytest-httpx to mock all HTTP calls — no real network requests are made and no API keys are needed to run the tests.

All 51 tests must pass before a pull request can be merged. CI runs the full suite against Python 3.9, 3.10, 3.11, and 3.12 on every push.


Releasing a new version

Releases are automated via GitHub Actions. Only maintainers with push access to main can trigger a release.

Steps:

  1. Update the version number in two places:

    • pyproject.tomlversion = "0.2.0"
    • tangentopay/__init__.py__version__ = "0.2.0"
  2. Update CHANGELOG.md with what changed in this version

  3. Commit and push to main:

    git add pyproject.toml tangentopay/__init__.py CHANGELOG.md
    git commit -m "chore: bump version to 0.2.0"
    git push origin main
    
  4. Tag the release — this triggers the publish workflow:

    git tag v0.2.0
    git push --tags
    
  5. GitHub Actions builds the wheel and source dist, authenticates to PyPI via OIDC (no stored secrets), and publishes automatically. Watch the progress under the Actions tab.

Note: Tags can only be pushed from the main branch. The pypi GitHub Actions environment is restricted to main only.


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)
  • 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.1.1.tar.gz (37.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.1.1-py3-none-any.whl (38.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tangentopay-0.1.1.tar.gz
  • Upload date:
  • Size: 37.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.1.1.tar.gz
Algorithm Hash digest
SHA256 502c9a64486200a6c53b7b409be96aba2c241b9359d1addf662f5979bcb7e0c8
MD5 12ddaad357346120ab0cf8eaef71f3fa
BLAKE2b-256 cc04c08ab7a4d2eed6db4eab5f07fe6f18140e0e3213eb6f594d6d6c8126dc19

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tangentopay-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 38.6 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.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 70aa761cbfdd4c69c3ddd4e5ee1cd35612ae8992c207db9eb6078cf1bf6fdebf
MD5 73fd3617937d2af9baeef8753f860e10
BLAKE2b-256 6b116a4cc4f2190dc5af9bf1bb0003de72b268e436d5d0981cea9377b8b97ff9

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