Skip to main content

Hosted Payment Page (HPP)

The Hosted Payment Page lets you accept payments without handling card details directly. You redirect the customer to a secure, Flowlix-hosted page where they enter their card information. After payment, the customer is sent back to your site.

Why use HPP?

  • No PCI scope — Flowlix handles all card data on our servers.
  • Less development effort — No need to build a card input form.
  • Consistent UX — A professional, mobile-optimized payment page.
  • Security — Card details never touch your servers.

How it works

  1. The customer clicks “Pay” on your site.
  2. Your server creates an HPP session via POST /v1/payments/hpp.
  3. You redirect the customer to the redirect_url from the response.
  4. The customer enters their card details on the Flowlix payment page.
  5. After payment, the customer is redirected to your redirect_url with query parameters.
  6. Your server fetches the payment to verify the status.

Step 1: Create an HPP session

curl -X POST https://api.flowlix.dev/v1/payments/hpp \
  -H "Authorization: Bearer fl_test_sk_abc123def456" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hpp-order-5678" \
  -d '{
    "amount": 2500,
    "currency": "eur",
    "customer": {
      "email": "alex@example.com",
      "first_name": "Alex",
      "last_name": "Johnson",
      "country": "DE",
      "phone": "+491709876543",
      "address": "Friedrichstrasse 100",
      "city": "Berlin",
      "zip": "10117"
    },
    "redirect_url": "https://shop.example.com/checkout/complete"
  }'

Request parameters

ParameterRequiredDescription
amountYesAmount in minor units (e.g. 2500 = EUR 25.00).
currencyYesThree-letter ISO 4217 code: eur, usd, or gbp.
redirect_urlYesWhere to send the customer after they complete or abandon the payment.
customerNoCustomer details object. Optional, but if you supply it every sub-field below is required.
customer.emailConditionalCustomer email.
customer.first_nameConditionalCustomer first name.
customer.last_nameConditionalCustomer last name.
customer.countryConditionalCountry code (ISO 3166-1 alpha-2).
customer.phoneConditionalPhone in international format.
customer.addressConditionalStreet address.
customer.cityConditionalCity.
customer.zipConditionalPostal code.
descriptionNoFree-text description for your records (max 500 chars).
metadataNoKey-value pairs for custom data (max 50 keys, each value up to 500 characters).
callback_urlNoServer-to-server callback URL. Reserved for future use; do not rely on it as your primary status signal. Always verify the final payment state via GET /v1/payments/{id}.

Response

{
  "id": "pay_b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "object": "payment",
  "amount": 2500,
  "currency": "eur",
  "status": "pending",
  "description": null,
  "card": null,
  "customer": null,
  "metadata": null,
  "decline_code": null,
  "decline_message": null,
  "redirect_url": "https://pay.flowlix.dev/hpp/pay_b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "refunded_at": null,
  "succeeded_at": null,
  "failed_at": null,
  "created": 1719792000,
  "livemode": false,
  "refunded_amount": 0,
  "provider_transaction_id": null,
  "refunds": [],
  "next_action": null
}
The payment starts with "status": "pending" and "card": null because the customer has not entered their card details yet. Note that the create response only echoes the fields the API has at this point — customer arrives as null even if you sent it; the upstream-resolved customer object is filled in on the next GET /v1/payments/{id} once the customer completes the page.

Step 2: Redirect the customer

Redirect the customer to the redirect_url from the previous response:
# Flask example
from flask import redirect

@app.route("/checkout/pay")
def pay():
    # ... create HPP session ...
    return redirect(payment["redirect_url"])
// Express example
app.get("/checkout/pay", async (req, res) => {
  // ... create HPP session ...
  res.redirect(payment.redirect_url);
});

Step 3: Handle the redirect back

After the customer completes (or abandons) the payment, Flowlix redirects them to your redirect_url with query parameters:
https://shop.example.com/checkout/complete?payment_id=pay_b2c3d4e5-f6a7-8901-bcde-f23456789012&status=succeeded
Query parameterDescription
payment_idThe payment ID.
statusThe terminal payment status: succeeded or failed.
Always verify the payment status server-side. Do not trust the query parameters alone — a malicious user could modify them. Fetch the payment from the API to confirm the actual status.

Step 4: Verify the payment

Fetch the payment from your server to confirm the status:
curl https://api.flowlix.dev/v1/payments/pay_b2c3d4e5-f6a7-8901-bcde-f23456789012 \
  -H "Authorization: Bearer fl_test_sk_abc123def456"
{
  "id": "pay_b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "object": "payment",
  "amount": 2500,
  "currency": "eur",
  "status": "succeeded",
  "card": {
    "brand": "visa",
    "last4": "1111",
    "exp_month": 12,
    "exp_year": 2027,
    "country": "US"
  },
  "customer": {
    "email": "alex@example.com",
    "name": "Alex Johnson"
  },
  "metadata": null,
  "decline_code": null,
  "decline_message": null,
  "redirect_url": null,
  "refunded_at": null,
  "succeeded_at": 1719792180,
  "failed_at": null,
  "created": 1719792000,
  "livemode": false,
  "refunded_amount": 0,
  "provider_transaction_id": null,
  "refunds": [],
  "next_action": null
}
On GET /v1/payments/{id}, redirect_url is reported as null even for HPP payments — the hosted page URL is only returned in the POST create response. Store it on your side if you need to send the customer back to the same page later.

Full integration example

import uuid
import requests
from flask import Flask, redirect, request, render_template

app = Flask(__name__)
FLOWLIX_API_KEY = "fl_test_sk_abc123def456"
BASE_URL = "https://api.flowlix.dev"


@app.route("/checkout/pay", methods=["POST"])
def create_checkout():
    """Create HPP session and redirect customer."""
    order = get_current_order()  # Your order logic

    response = requests.post(
        f"{BASE_URL}/v1/payments/hpp",
        headers={
            "Authorization": f"Bearer {FLOWLIX_API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": f"hpp-{order.id}",
        },
        json={
            "amount": order.total_in_cents,
            "currency": "eur",
            "customer": {
                "email": order.customer_email,
                "first_name": order.customer_first_name,
                "last_name": order.customer_last_name,
                "country": order.customer_country,
                "phone": order.customer_phone,
                "address": order.customer_address,
                "city": order.customer_city,
                "zip": order.customer_zip,
            },
            "redirect_url": f"https://shop.example.com/checkout/{order.id}/complete",
        },
        timeout=30,
    )
    if response.status_code != 201:
        error = response.json().get("error", {})
        app.logger.error("HPP create failed: %s", error)
        return render_template("checkout_error.html", error=error), 502

    payment = response.json()
    order.payment_id = payment["id"]
    order.hpp_url = payment["redirect_url"]
    order.save()

    return redirect(payment["redirect_url"])


@app.route("/checkout/<order_id>/complete")
def checkout_complete(order_id):
    """Handle redirect back from Flowlix HPP."""
    order = get_order(order_id)
    payment_id = request.args.get("payment_id")

    # Always verify server-side
    response = requests.get(
        f"{BASE_URL}/v1/payments/{payment_id}",
        headers={"Authorization": f"Bearer {FLOWLIX_API_KEY}"},
        timeout=30,
    )
    response.raise_for_status()
    payment = response.json()

    if payment["status"] == "succeeded":
        order.mark_paid()
        return render_template("success.html", order=order)
    elif payment["status"] in ("failed", "expired"):
        return render_template("failed.html", order=order, payment=payment)
    else:
        # `pending` should be rare here — the customer was just bounced back —
        # but it can happen briefly. Show a "still processing" page and refresh.
        return render_template("processing.html", order=order)

Best practices

  • Always verify payment status server-side after the redirect. Never rely on query parameters alone.
  • Check response.status_code before reading response.json() — a 4xx from create will not contain a payment object and will crash naive code.
  • Store the payment_id in your database when you create the HPP session, so you can reconcile later.
  • Use the Idempotency-Key tied to your order ID to prevent creating duplicate payment sessions.
  • Handle all terminal statuses in your redirect handler: succeeded, failed, and expired. Treat unexpected pending as “still processing — retry shortly”.
  • Set a redirect_url that your customer recognizes and trusts (use your own domain).