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
- The customer clicks “Pay” on your site.
- Your server creates an HPP session via
POST /v1/payments/hpp.
- You redirect the customer to the
redirect_url from the response.
- The customer enters their card details on the Flowlix payment page.
- After payment, the customer is redirected to your
redirect_url with query parameters.
- 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
| Parameter | Required | Description |
|---|
amount | Yes | Amount in minor units (e.g. 2500 = EUR 25.00). |
currency | Yes | Three-letter ISO 4217 code: eur, usd, or gbp. |
redirect_url | Yes | Where to send the customer after they complete or abandon the payment. |
customer | No | Customer details object. Optional, but if you supply it every sub-field below is required. |
customer.email | Conditional | Customer email. |
customer.first_name | Conditional | Customer first name. |
customer.last_name | Conditional | Customer last name. |
customer.country | Conditional | Country code (ISO 3166-1 alpha-2). |
customer.phone | Conditional | Phone in international format. |
customer.address | Conditional | Street address. |
customer.city | Conditional | City. |
customer.zip | Conditional | Postal code. |
description | No | Free-text description for your records (max 500 chars). |
metadata | No | Key-value pairs for custom data (max 50 keys, each value up to 500 characters). |
callback_url | No | Server-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 parameter | Description |
|---|
payment_id | The payment ID. |
status | The 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).