Hosted Payment Page Integration

Build a complete HPP integration that creates a session, redirects the customer to a branded payment page, and receives a webhook when payment completes.

How it works

Prerequisites

  • A CatalystPay API key (rk_...)
  • A campaign ID with a hosted page configured (ask your CatalystPay admin)
  • A publicly accessible webhook endpoint (use webhook.site for testing)

Step 1: Create a payment session

When your customer clicks "Pay" on your site, your server creates a payment session.

curl -X POST https://api.catalystpay.com/api/v1/payment-sessions/ \
  -H "Authorization: Bearer rk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "lead": {
      "first_name": "Max",
      "last_name": "Mustermann",
      "email": "[email protected]",
      "country": "DE",
      "address": "Musterstrasse 42",
      "city": "Berlin",
      "postal_code": "10115"
    }
  }'
import requests

response = requests.post(
    "https://api.catalystpay.com/api/v1/payment-sessions/",
    headers={
        "Authorization": "Bearer rk_your_api_key",
        "Content-Type": "application/json",
    },
    json={
        "campaign_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "lead": {
            "first_name": "Max",
            "last_name": "Mustermann",
            "email": "[email protected]",
            "country": "DE",
            "address": "Musterstrasse 42",
            "city": "Berlin",
            "postal_code": "10115",
        },
    },
)

session = response.json()
payment_url = session["payment_url"]
const response = await fetch(
  "https://api.catalystpay.com/api/v1/payment-sessions/",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer rk_your_api_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      campaign_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      lead: {
        first_name: "Max",
        last_name: "Mustermann",
        email: "[email protected]",
        country: "DE",
        address: "Musterstrasse 42",
        city: "Berlin",
        postal_code: "10115",
      },
    }),
  }
);

const session = await response.json();
const paymentUrl = session.payment_url;

Response:

{
  "session_token": "ps_abc123def456ghi789jkl012mno345",
  "status": "PENDING",
  "payment_url": "https://pay.catalystpay.com/s/ps_abc123def456ghi789jkl012mno345"
}

Step 2: Redirect the customer

Send the customer's browser to the payment_url from the response. The hosted payment page displays your campaign branding, offer details, and a payment form.

# In a Django/Flask view:
from django.shortcuts import redirect

def checkout_view(request):
    # ... create session (Step 1) ...
    return redirect(session["payment_url"])
// Client-side redirect:
window.location.href = paymentUrl;
Open in a browser:
https://pay.catalystpay.com/s/ps_abc123def456ghi789jkl012mno345

The customer sees the hosted payment page. They enter their IBAN and account holder name, then submit. CatalystPay handles validation, payment submission, and any gateway-specific verification steps.

After a successful payment, the customer is redirected to the success_redirect_url configured on your hosted page. If the payment fails after all retry attempts, they go to the failure_redirect_url.

Step 3: Receive the webhook

When the payment completes, CatalystPay sends a payment_session.completed webhook to your configured endpoint.

The webhook arrives as a POST request with these headers:

Content-Type: application/json
X-CatalystPay-Event: payment_session.completed
X-CatalystPay-Signature: a3f2b1c4d5e6...

The payload contains the fields you mapped in your webhook profile. A typical payload looks like:

{
  "session_token": "ps_abc123def456ghi789jkl012mno345",
  "status": "COMPLETED",
  "order_number": "ORD-a1b2c3d4",
  "amount": "29.99",
  "currency": "EUR",
  "email": "[email protected]",
  "customer_name": "Max Mustermann"
}

Step 4: Verify the webhook signature

Before processing the webhook, verify the HMAC-SHA256 signature to confirm it came from CatalystPay.

import hmac
import hashlib
import json

def verify_webhook(payload_body: bytes, signature: str, signing_secret: str) -> bool:
    # Reconstruct canonical JSON (sorted keys, compact separators)
    payload_dict = json.loads(payload_body)
    canonical = json.dumps(payload_dict, sort_keys=True, separators=(",", ":"))

    expected = hmac.new(
        signing_secret.encode("utf-8"),
        canonical.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
const crypto = require("crypto");

// Recursively sort object keys to match Python's sort_keys=True
function sortKeys(obj) {
  if (typeof obj !== "object" || obj === null) return obj;
  if (Array.isArray(obj)) return obj.map(sortKeys);
  return Object.keys(obj).sort().reduce((acc, key) => {
    acc[key] = sortKeys(obj[key]);
    return acc;
  }, {});
}

function verifyWebhook(payloadBody, signature, signingSecret) {
  // Reconstruct canonical JSON (sorted keys, compact separators)
  const payload = JSON.parse(payloadBody);
  const canonical = JSON.stringify(sortKeys(payload));

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(canonical)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
# Manual check using openssl:
echo -n '{"amount":"29.99","currency":"EUR","email":"[email protected]"}' \
  | openssl dgst -sha256 -hmac "your_signing_secret"

Always verify the signature before processing webhook data. See the Webhooks guide for full details on the signing algorithm.

Step 5: Confirm the session status

As a final check, retrieve the session to confirm the status matches the webhook.

curl https://api.catalystpay.com/api/v1/payment-sessions/ps_abc123def456ghi789jkl012mno345/
response = requests.get(
    "https://api.catalystpay.com/api/v1/payment-sessions/ps_abc123def456ghi789jkl012mno345/"
)
print(response.json()["status"])  # "COMPLETED"
const res = await fetch(
  "https://api.catalystpay.com/api/v1/payment-sessions/ps_abc123def456ghi789jkl012mno345/"
);
const data = await res.json();
console.log(data.status); // "COMPLETED"

The status field reads "COMPLETED". Your HPP integration is working end-to-end.

What you learned

  • Your server creates sessions; CatalystPay handles the payment UI
  • The payment_url in the response is where customers complete payment
  • Webhooks notify your server when payment completes
  • Always verify the X-CatalystPay-Signature before processing webhooks

Next steps