Docs / Patterns / FastAPI

FastAPI.

Server route that creates an invoice and redirects to hosted checkout, and a webhook handler that verifies the signature and reacts to invoice.paid. Both files are paste-and-go.

Install.

$ pip install settle-sdk httpx

Env vars.

Set these on your server. SETTLE_API_KEY is your sk_test_… or sk_live_… key from the dashboard. SETTLE_WEBHOOK_SECRET is returned by register_webhook when you set up the endpoint.

  • SETTLE_API_KEY
  • SETTLE_WEBHOOK_SECRET

Server route.

Receives a POST from your frontend with the order amount and customer email, creates an invoice via the REST API, and 303-redirects the customer to the hosted checkout URL.

app/checkout.py
# app/checkout.py
import os
import httpx
from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse, JSONResponse

router = APIRouter()

@router.post("/api/checkout")
async def create_checkout(request: Request):
    body = await request.json()
    origin = str(request.base_url).rstrip("/")
    async with httpx.AsyncClient(timeout=10.0) as client:
        res = await client.post(
            "https://api.settle.xxx/v1/invoices",
            headers={"Authorization": f"Bearer {os.environ['SETTLE_API_KEY']}"},
            json={
                "amount_usd": body["amount_usd"],
                "customer_email": body.get("customer_email"),
                "metadata": body.get("metadata"),
                "return_url": f"{origin}/orders/success",
                "cancel_url": f"{origin}/cart",
            },
        )
    if res.status_code >= 400:
        return JSONResponse({"error": "checkout_failed"}, status_code=502)
    invoice = res.json()
    return RedirectResponse(invoice["checkout_url"], status_code=303)

Webhook handler.

Verifies the HMAC-SHA256 signature on the raw request body, then handles invoice.paid. Read Webhooks for the event list and retry semantics.

app/webhooks.py
# app/webhooks.py
import hmac
import hashlib
import os
from fastapi import APIRouter, Request, HTTPException

router = APIRouter()

@router.post("/webhooks/settle")
async def settle_webhook(request: Request):
    signature = request.headers.get("settle-signature")
    if not signature:
        raise HTTPException(status_code=400, detail="missing signature")

    raw = await request.body()
    secret = os.environ["SETTLE_WEBHOOK_SECRET"].encode()
    expected = hmac.new(secret, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="bad signature")

    event = await request.json()
    if event["type"] == "invoice.paid":
        # Mark order paid.
        pass
    return {"received": True}

Notes.

Use hmac.compare_digest for the signature check — never == on bytes. Read the raw body with await request.body() before any json parsing, otherwise FastAPI's middleware can mutate it.

Was this helpful?