TchokoPay
Examples

Python — full example

Charge a customer and handle the webhook, end to end.

The same integration as the Node.js example, using Flask.

import hmac
import hashlib
import os
import time

import requests
from flask import Flask, request, abort, redirect

app = Flask(__name__)

TCHOKO_KEY = os.environ["TCHOKOPAY_API_KEY"]
WEBHOOK_SECRET = os.environ["TCHOKOPAY_WEBHOOK_SECRET"]


def charge_customer(order_id: str, amount_xaf: int) -> str:
    response = requests.post(
        "https://connect.tchokopay.com/v1/payments",
        headers={
            "Authorization": f"Bearer {TCHOKO_KEY}",
            "Idempotency-Key": f"charge-{order_id}",
        },
        json={
            "amount": amount_xaf,
            "currency": "XAF",
            "description": f"Order {order_id}",
            "reference": order_id,
        },
    )
    if not response.ok:
        raise RuntimeError(f"TchokoPay error: {response.json()['message']}")

    return response.json()["checkoutUrl"]  # redirect your customer here


def verify_webhook(raw_body: bytes, signature_header: str, timestamp_header: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        f"{timestamp_header}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()

    provided = signature_header.replace("sha256=", "")
    valid = hmac.compare_digest(expected, provided)

    age = abs(time.time() - float(timestamp_header))
    if age > 300:
        return False  # stale — reject even if the signature matches

    return valid


@app.get("/checkout/<order_id>")
def checkout(order_id):
    checkout_url = charge_customer(order_id, 5000)
    return redirect(checkout_url)


@app.post("/webhooks/tchokopay")
def tchokopay_webhook():
    signature = request.headers.get("X-TchokoPay-Signature", "")
    timestamp = request.headers.get("X-TchokoPay-Timestamp", "")

    if not verify_webhook(request.get_data(), signature, timestamp, WEBHOOK_SECRET):
        abort(400, "invalid signature")

    event = request.get_json()

    if event["status"] == "SUCCESS":
        mark_order_paid(event["invoiceReference"], event["settledAmount"], event["settledCurrency"])
    elif event["status"] == "FAILED":
        mark_order_failed(event["invoiceReference"], event.get("failureReason"))

    return "", 200

request.get_data() returns the raw request bytes — don't call request.get_json() first and re-serialize it for signature verification, that can reorder keys and break the signature match.