Skip to main content
Always verify webhook signatures before processing any event. Skipping this step opens your application to spoofed requests.

How Signatures Work

HOST Pay signs every outgoing webhook using HMAC-SHA256. The signing input is:
timestamp + "." + compact_json_body
Where:
  • timestamp is the Unix epoch value from the X-Webhook-Timestamp header (as a string).
  • compact_json_body is the raw request body serialized with no extra whitespace.
The resulting signature is hex-encoded and placed in the X-HostPay-Signature header with a v1= prefix (e.g., v1=abc123...).

Verification Steps

  1. Extract the raw timestamp from X-Webhook-Timestamp.
  2. Read the raw request body before parsing it as JSON.
  3. Construct the signed string: timestamp + "." + raw_body.
  4. Compute HMAC-SHA256(secret, signed_string) and hex-encode it.
  5. Compare your result to the value after the v1= prefix using a constant-time comparison.
  6. Optional but recommended: Reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.
The official SDKs implement all of the steps above — constant-time comparison and replay protection included. Pass the raw request body and headers straight from your framework:
from fastapi import FastAPI, HTTPException, Request
from hostpay import HostPay, SignatureVerificationError

app = FastAPI()
client = HostPay(api_key="YOUR_API_KEY", secret_key="YOUR_SECRET_KEY")

@app.post("/webhooks/hostpay")
async def hostpay_webhook(request: Request):
    try:
        event = client.webhooks.construct_event(
            payload=await request.body(),   # raw bytes, not request.json()
            headers=request.headers,
            secret=WEBHOOK_SIGNING_SECRET,
        )
    except SignatureVerificationError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    if event.event == "deposit.completed":
        ...  # handle it
    return {"ok": True}
Deliveries older than 5 minutes are rejected by default (pass a custom tolerance in seconds to change this).

Verifying Manually

Implementing the check yourself in another language:
from fastapi import FastAPI, HTTPException, Request
import hmac
import hashlib
import time
import json

app = FastAPI()
WEBHOOK_SECRET = "your_webhook_secret"

@app.post("/webhooks/hostpay")
async def handle_webhook(request: Request):
    # Read raw body before any parsing
    raw_body = (await request.body()).decode("utf-8")

    # Extract headers
    signature_header = request.headers.get("X-HostPay-Signature", "")
    timestamp = request.headers.get("X-Webhook-Timestamp", "")

    # Reject stale webhooks (optional but recommended)
    if abs(time.time() - int(timestamp)) > 300:
        raise HTTPException(status_code=400, detail="Webhook timestamp too old")

    # Verify the signature
    if not verify_signature(raw_body, timestamp, signature_header):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # Parse and handle the event
    event = json.loads(raw_body)
    data = event.get("data", {})

    # Route by status in the data payload
    status = data.get("status")
    if status == "succeeded":
        handle_success(data)
    elif status == "failed":
        handle_failure(data)

    return {"ok": True}


def verify_signature(raw_body: str, timestamp: str, signature_header: str) -> bool:
    # Signature header format: "v1=<hex_digest>"
    if not signature_header.startswith("v1="):
        return False
    provided_sig = signature_header[3:]

    signed_string = f"{timestamp}.{raw_body}"
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        signed_string.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(provided_sig, expected_sig)


def handle_success(data):
    print(f"Transaction {data['id']} succeeded: {data['amount']} {data['currency']}")


def handle_failure(data):
    print(f"Transaction {data['id']} failed")
Read the raw request body before any JSON parsing — re-serializing the parsed body changes whitespace and breaks signature verification.