Paylinq API Reference
Accept payments in your application with a single REST integration. Paylinq orchestrates multiple payment providers (Stripe, Airwallex, Mollie, PayPal) and automatically routes each transaction to the best available provider using priority ordering, circuit breaking, and round-robin load distribution.
Integration in 3 steps:
- 1Create a payment
POST /v1/paymentsPass amount, currency, and optionally customer details. You get back a payment_url.
- 2Send your customer to checkout
payment_urlRedirect to our hosted pay page or embed it as an iframe on your site.
- 3Receive a webhook
payment.succeededWe POST to your webhook URL when the payment completes. Update your order status.
Base URL
https://api.paylinq.netContent-Type
application/jsonAmounts
Always in smallest unit (cents). 2500 = €25.00
Rate limit
100 requests / minute per API key
Payment Statuses
| Status | Meaning |
|---|---|
| pending | Payment created, awaiting customer interaction |
| processing | Customer has initiated a payment method; awaiting provider confirmation |
| requires_action | 3D-Secure or additional authentication required from the customer |
| succeeded | Payment completed successfully — money has been captured |
| failed | All provider attempts failed |
| cancelled | Payment was cancelled before completion |
| refunded | Full refund issued |
| partially_refunded | Partial refund issued; original amount – refunded_amount remains captured |
Authentication
Every API request must include your API key in the X-API-Key header. Retrieve your key from the Apps page of the dashboard. Each app has its own key.
curl https://api.paylinq.net/v1/payments \
-H "X-API-Key: plq_live_YOUR_API_KEY"Idempotency
The Paylinq API supports idempotent requests on payment creation and refund creation. This lets you safely retry a request after a network failure without the risk of creating duplicate payments or double-debiting funds.
How it works
Add an Idempotency-Key header containing any unique string (UUID recommended) to your request. If you repeat the same request with the same key within 24 hours, Paylinq returns the original response instead of creating a new resource. After 24 hours the key expires and a new resource can be created with the same key.
Keys are scoped per app — the same key value sent by two different API keys is treated independently.
Creating a payment safely
curl -X POST https://api.paylinq.net/v1/payments \
-H "X-API-Key: plq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_1234_attempt_1" \
-d '{"amount": 2500, "currency": "EUR", "description": "Order #1234"}'
# Retrying with the same key returns the original payment — no duplicate charge:
curl -X POST https://api.paylinq.net/v1/payments \
-H "X-API-Key: plq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_1234_attempt_1" \
-d '{"amount": 2500, "currency": "EUR", "description": "Order #1234"}'Creating a refund safely
curl -X POST https://api.paylinq.net/v1/refunds \
-H "X-API-Key: plq_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund_order_1234_full" \
-d '{"payment_id": "a1b2c3d4-...", "reason": "Customer request"}'Key guidelines
| Generate per action | Use a fresh UUID per payment or refund. Do not reuse a key for a different amount or a different customer. |
| Persist the key | Store the key alongside your order before sending the request. On retry, read it back and send the same value. |
| 24-hour window | A key expires 24 hours after the original request. After expiry, the same key may create a new resource. |
| Scoped per app | Keys from different API keys never collide — each app has its own idempotency namespace. |
| Optional header | If omitted, the request always creates a new resource. We strongly recommend always including it. |
Payments
A payment represents a single transaction. Create one server-side, redirect your customer to the hosted checkout page, and receive a webhook when it completes. All amounts are in the smallest currency unit (cents).
Customers
Customer records store contact details and link to all payments made by that person. Paylinq automatically creates and matches customers when you pass customer_email on a payment — you only need these endpoints if you want to manage customers directly.
customer_email and customer_name directly on POST /v1/payments. Paylinq will find an existing customer by email or create a new one automatically. You don't need to pre-create customers.Refunds
Issue full or partial refunds against succeeded payments. Paylinq routes the refund back through the same provider that processed the original payment.
Coupon Codes
Discount codes with percentage or fixed-amount reductions, optional usage limits, minimum amounts, and validity date ranges. Codes can be scoped to one or more products — when scoped, the code only applies to payments that reference one of those products. Customers can enter codes on the hosted checkout page, or you can create and validate them server-side.
Payment Links
Payment links are reusable checkout URLs — unlike one-off payments they are not bound to a single customer session and can be shared via email, social media, or QR codes. Each visit to the link creates a new underlying payment automatically.
Link behaviour
| Each visit | Creates a fresh payment with the configured amount, currency, and product |
| Customer data | The customer's IP address is captured on page load for fraud detection |
| Coupons | Customers can enter discount codes directly on the payment page |
| Webhooks | Each completed payment fires the usual payment.succeeded webhook |
| Expiry | Links can be time-limited; expired links show an appropriate message |
Shareable link format
https://app.paylinq.net/shop/{link_id}
https://app.paylinq.net/shop/{link_id}/product/{product_id} // single-product checkoutSettlements
Settlements are weekly consolidated reports generated every Monday. They group all succeeded payments from the prior week, calculate provider fees, and break down volume per provider.
Webhooks
When a payment status changes, Paylinq sends an HTTP POST to the webhook URL you configured for your app. Configure the URL in the Apps section of the dashboard.
X-Paylinq-Signature must be rejected.Events
| Event | When it fires |
|---|---|
| payment.succeeded | Payment completed — money captured. Safe to fulfil the order. |
| payment.failed | All provider attempts have failed. Customer should retry or use a different method. |
| payment.refunded | A full or partial refund was issued. |
| payment.cancelled | Payment was explicitly cancelled (status set to cancelled). |
| payment.requires_action | Awaiting customer action (e.g. 3D-Secure). No fulfilment action needed yet. |
HTTP headers sent with every webhook
| Content-Type | application/json |
| X-Paylinq-Signature | HMAC-SHA256 hex digest of the raw JSON body, keyed with your app's webhook_secret |
| X-Paylinq-Event | Event name (e.g. payment.succeeded) |
Payload shape
{
"id": "wh_del_abc123-...",
"event": "payment.succeeded",
"created": "2026-04-15T10:35:00Z",
"data": {
"payment_id": "a1b2c3d4-...",
"external_ref": "order_1234",
"amount": 2500, // original order amount before any coupon discount (smallest currency unit)
"discount_amount": 0, // coupon discount applied (0 if no coupon); amount - discount_amount = charged amount
"currency": "EUR",
"status": "succeeded"
}
}
// payment.refunded carries additional fields:
{
"event": "payment.refunded",
"data": {
"payment_id": "a1b2c3d4-...",
"refund_id": "ref_abc123-...",
"amount": 500,
"currency": "EUR"
}
}Signature verification
Compute HMAC-SHA256 of the raw request body bytes using your app's webhook_secretand compare it to the X-Paylinq-Signature header using a constant-time comparison.
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret" # From dashboard → Apps
@app.route("/webhooks/paylinq", methods=["POST"])
def handle_webhook():
body = request.get_data() # raw bytes — do NOT parse first
sig = request.headers.get("X-Paylinq-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401, "Invalid signature")
event = request.json
match event["event"]:
case "payment.succeeded":
payment_id = event["data"]["payment_id"]
order_ref = event["data"]["external_ref"]
# fulfil_order(order_ref)
case "payment.refunded":
refund_id = event["data"]["refund_id"]
# process_refund(refund_id)
case "payment.failed":
pass # notify customer, no action needed
return {"received": True}, 200Embed / Widget
Instead of redirecting customers to payment_url, embed the Paylinq checkout directly in your page. The widget communicates status changes back to your page via postMessage.
Option 1 — Script tag (simplest)
Drop this snippet into your HTML. The widget mounts as a full-page overlay:
<!-- Step 1: create a payment server-side (POST /v1/payments), get its id -->
<!-- Step 2: embed the widget with that id -->
<script
src="https://app.paylinq.net/widget.js"
data-payment-id="PAYMENT_ID"
></script>Option 2 — iframe with postMessage
<iframe
id="paylinq-checkout"
src="https://app.paylinq.net/pay/PAYMENT_ID?embed=1"
width="100%"
height="620"
frameborder="0"
allow="payment"
style="border:none; border-radius:12px;"
></iframe>
<script>
window.addEventListener("message", (event) => {
// Always verify origin in production!
if (!event.origin.startsWith("https://app.paylinq.net")) return;
switch (event.data.type) {
case "paylinq:payment:succeeded":
// event.data.payload = { paymentId, status }
document.getElementById("paylinq-checkout").style.display = "none";
showSuccessPage(event.data.payload.paymentId);
break;
case "paylinq:payment:failed":
showRetryMessage();
break;
case "paylinq:payment:requires_action":
// 3DS or similar — the widget handles it automatically
break;
}
});
</script>Option 3 — React component
import { useEffect } from "react";
interface PaylinqCheckoutProps {
paymentId: string;
onSuccess?: (data: { paymentId: string; status: string }) => void;
onFailure?: (data: { paymentId: string; status: string }) => void;
}
export function PaylinqCheckout({ paymentId, onSuccess, onFailure }: PaylinqCheckoutProps) {
useEffect(() => {
const handler = (e: MessageEvent) => {
if (!e.origin.startsWith("https://app.paylinq.net")) return;
if (e.data.type === "paylinq:payment:succeeded") onSuccess?.(e.data.payload);
if (e.data.type === "paylinq:payment:failed") onFailure?.(e.data.payload);
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [onSuccess, onFailure]);
return (
<iframe
src={`https://app.paylinq.net/pay/${paymentId}?embed=1`}
width="100%"
height="620"
frameBorder="0"
allow="payment"
style={{ border: "none", borderRadius: 12 }}
/>
);
}
// Usage:
// const payment = await createPayment({ amount: 2500, currency: "EUR" });
// <PaylinqCheckout
// paymentId={payment.id}
// onSuccess={(d) => router.push("/success?id=" + d.paymentId)}
// onFailure={() => setError("Payment failed. Please try again.")}
// />postMessage event types
| type | When | payload |
|---|---|---|
| paylinq:payment:succeeded | Payment completed | { paymentId, status } |
| paylinq:payment:failed | All attempts failed | { paymentId, status } |
| paylinq:payment:requires_action | 3DS in progress | { paymentId } |
Error Reference
All error responses have HTTP status ≥ 400 and a JSON body with a detail field:
{
"detail": "Human-readable error message"
}| Status | Meaning | What to do |
|---|---|---|
| 400 | Bad Request | Missing or invalid field in the request body. Check the `detail` message. |
| 401 | Unauthorized | Missing or incorrect X-API-Key header. |
| 404 | Not Found | The ID you referenced (payment, customer, refund) does not exist in your account. |
| 409 | Conflict | Duplicate action (e.g. settlement already generated for the period). |
| 422 | Processing Failed | Request was valid but the provider rejected it. Check `detail` for the provider error. |
| 429 | Rate Limited | Exceeded 100 requests/minute. Wait and retry with backoff. |
| 500 | Internal Server Error | Unexpected error on Paylinq's side. Retry after a short delay. Contact support if it persists. |
| 502 | Provider Error | Paylinq could not reach the underlying payment provider. Retry after a short delay. |
Retrying failed requests
Safe to retry: 429, 500, 502, and any network error. Use exponential backoff: 1 s, 2 s, 4 s, 8 s… Do not retry 400, 401, or 404 — they are client errors and will not resolve on retry.
Idempotency-Key on every retry attempt. This guarantees that even if your first request reached the server but the response was lost, the retry returns the original result rather than creating a duplicate charge.import uuid, time, requests
def paylinq_post(path, body, api_key, max_retries=3):
url = "https://api.paylinq.net" + path
idem_key = str(uuid.uuid4()) # generate once, reuse on every retry
headers = {
"X-API-Key": api_key,
"Content-Type": "application/json",
"Idempotency-Key": idem_key,
}
for attempt in range(max_retries):
r = requests.post(url, headers=headers, json=body)
if r.status_code < 500:
return r.json() # success or client error — don't retry
wait = 2 ** attempt
print(f"Server error, retrying in {wait}s...")
time.sleep(wait)
r.raise_for_status()