Use the /payment-links endpoint to generate a hosted payment page. Instead of building a custom checkout UI, you redirect the payer to a HelloPay-hosted URL where they choose from the payment methods enabled for your account.
You can keep the link fully open, or optionally send defaults such as rail, inlineCustomer, or pse.bank when your flow already knows the payer, preferred payment method, or selected PSE bank.
How it works
- Send a
POST request to /payment-links with the amount, your internal reference, a callbackUrl, and any optional defaults for the checkout flow.
- HelloPay returns a
paymentLinkUrl — the full URL to redirect the payer to.
- Redirect the payer to the
paymentLinkUrl.
- The payer selects their preferred payment method and completes the flow on the HelloPay-hosted page.
- HelloPay fires a webhook event when the payment link is completed or expires.
- Once the flow ends (whether successful or not), the payer is redirected to the
callbackUrl you provided.
Redirecting the payer
The paymentLinkUrl in the response is the full URL of the hosted payment page. Redirect the payer to this URL as soon as you receive it.
{
"paymentLinkId": "4a8f2e1b-7c3d-4e5a-9b6c-8d1f2e3a4b5c",
"paymentLinkUrl": "https://pay.hellopay.com.co/link/V1JKU0tQNU02Wklp5lf2gAY8DYXizUKPzcG39x4i_A",
"status": "PENDING",
"createdAt": "2026-04-20T10:00:00.000Z",
"expiresAt": "2026-04-20T11:00:00.000Z",
"rail": null,
"inlineCustomer": null
}
The page will display the payment methods enabled for your merchant account. If you send rail, the payment link is restricted to that method and the payer cannot choose a different one. If you send inlineCustomer, HelloPay attaches that customer data to the payin created from the link.
Choosing the right flow
rail and inlineCustomer are optional. Include them only when they match the experience you want to build:
| Flow | Fields to send | When to use it |
|---|
| Open checkout | Omit optional checkout fields | The payer chooses the payment method and enters their own details. |
| Restricted payment method | Send rail only | You want to lock the payment link to a specific rail (PSE or BRE_B). The payer will not be able to choose a different method. |
| PSE bank preselection | Send rail: "PSE" and pse.bank | You already collected the payer’s PSE bank and want to attach it to the payment link. If omitted, the payer selects the bank in the hosted checkout. |
| Pre-filled customer | Send inlineCustomer only | You already collected customer details and want to skip asking for them again. |
| Restricted and pre-filled | Send rail and inlineCustomer | You know both the payer and the required payment method. |
When rail is set, it restricts the payment link to that rail only. The payer cannot select a different payment method. Omit rail if you want the payer to choose from all payment methods enabled for your account.
Payment links expire after 1 hour. The expiresAt field tells you exactly when. Do not attempt to reuse a link after it has expired — create a new one instead.
Handling webhook events
HelloPay fires payment-link webhook events when a link is completed or expires. You must have a webhook configured to receive these — see Webhooks for setup instructions.
Listen for the following events:
| Event | data.status | Meaning |
|---|
paymentlink.completed | COMPLETED | The payer completed the payment successfully. |
paymentlink.expired | EXPIRED | The payment link expired before the payer completed it. |
Payins created through a payment link do not fire payin.* webhook events. Use paymentlink.completed as the sole authoritative signal that a payment was collected. Do not rely on your existing payin webhook handlers to detect payment link payments — they will not be triggered.
Sample paymentlink.completed payload
{
"event": "paymentlink.completed",
"resource": "paymentlink",
"data": {
"paymentLinkId": "4a8f2e1b-7c3d-4e5a-9b6c-8d1f2e3a4b5c",
"reference": "XYZ",
"amount": 1000,
"currency": "COP",
"status": "COMPLETED",
"createdAt": "2026-04-19T14:32:15.300Z",
"updatedAt": "2026-04-19T14:32:20.395Z",
"expiresAt": null,
"completedAt": "2026-04-19T14:32:20.393Z"
}
}
Sample paymentlink.expired payload
{
"event": "paymentlink.expired",
"resource": "paymentlink",
"data": {
"paymentLinkId": "4a8f2e1b-7c3d-4e5a-9b6c-8d1f2e3a4b5c",
"reference": "XYZ",
"amount": 1000,
"currency": "COP",
"status": "EXPIRED",
"createdAt": "2026-04-19T14:32:15.300Z",
"updatedAt": "2026-04-19T15:32:15.300Z",
"expiresAt": "2026-04-19T15:32:15.300Z",
"completedAt": null
}
}
Correlating a webhook event to a payment link
Each webhook event payload includes a data object with the payment link details. Use either of these two fields to identify which payment link the event belongs to:
data.reference — matches the reference you passed when creating the payment link.
data.paymentLinkId — matches the paymentLinkId returned in the creation response.
// Example webhook handler
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'paymentlink.completed' || event === 'paymentlink.expired') {
const { reference, paymentLinkId, status } = data;
// status is 'COMPLETED' for paymentlink.completed, 'EXPIRED' for paymentlink.expired
// Use reference or paymentLinkId to look up the order in your system
console.log(`Payment link ${paymentLinkId} (ref: ${reference}) is now ${status}`);
}
res.sendStatus(200);
});
Callback redirect
After the payment flow ends — regardless of whether the payment succeeded or failed — HelloPay redirects the payer back to the callbackUrl you provided when creating the link. Use this URL to send the payer to a confirmation, error, or status page in your application.
The redirect to callbackUrl happens on the client side at the end of the payment flow. It is not a substitute for webhook events — always rely on webhooks for authoritative payment status updates.
Request fields
| Field | Type | Required | Description |
|---|
amountType | string | Yes | The only value currently supported is FIXED, which means the payer will be charged exactly amountInCents. |
amountInCents | number | Yes | Amount to collect in cents. |
reference | string | Yes | Your internal reference for this payment link. Returned in payment-link webhook events. |
callbackUrl | string | Yes | URL to redirect the payer to after the payment flow ends. |
rail | string | No | Restricts the payment link to a single rail. Accepted values: PSE, BRE_B. When set, the payer cannot choose a different payment method. Omit to allow the payer to choose from all enabled methods. |
inlineCustomer | object | No | Customer data to attach to the payin created from this link. If omitted, the payer enters their details in the checkout. |
pse | object | No | PSE-specific defaults for the payment link. Only send this object when rail is PSE. |
pse.bank | string | No | PSE bank selected by the payer. When pse is sent, bank must be present. Use one of the supported bank codes from the PSE payin guide. If omitted, the payer selects the bank in the hosted checkout. |
amountType currently only supports FIXED. Additional amount-handling modes may be introduced in the future.
Sample request
curl --request POST \
--url https://api.hellopay.com.co/payment-links \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"amountType": "FIXED",
"reference": "INV-2026-001",
"amountInCents": 100000,
"callbackUrl": "https://hellopay.com.co/callback",
"rail": "PSE",
"pse": {
"bank": "CO_BANCOLOMBIA"
},
"inlineCustomer": {
"name": "John Doe",
"idType": "CO_CC",
"idNumber": "1000000001",
"email": "john.doe@example.com",
"phone": "+573001234567"
}
}'
In this example, rail, pse.bank, and inlineCustomer are included to preconfigure the checkout. You can omit any optional field depending on your flow. Only send pse when rail is PSE.
Sample response
{
"paymentLinkId": "4a8f2e1b-7c3d-4e5a-9b6c-8d1f2e3a4b5c",
"paymentLinkUrl": "https://pay.hellopay.com.co/link/V1JKU0tQNU02Wklp5lf2gAY8DYXizUKPzcG39x4i_A",
"status": "PENDING",
"createdAt": "2026-04-20T10:00:00.000Z",
"expiresAt": "2026-04-20T11:00:00.000Z",
"rail": "PSE",
"inlineCustomer": {
"name": "John Doe",
"idType": "CO_CC",
"idNumber": "1000000001",
"email": "john.doe@example.com",
"phone": "+573001234567"
}
}
Use GET /payment-links/{paymentLinkId} to retrieve the current state of a link at any time — for example, to check whether a link has expired or been completed before deciding to create a new one.