Skip to main content
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

  1. Send a POST request to /payment-links with the amount, your internal reference, a callbackUrl, and any optional defaults for the checkout flow.
  2. HelloPay returns a paymentLinkUrl — the full URL to redirect the payer to.
  3. Redirect the payer to the paymentLinkUrl.
  4. The payer selects their preferred payment method and completes the flow on the HelloPay-hosted page.
  5. HelloPay fires a webhook event when the payment link is completed or expires.
  6. 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:
FlowFields to sendWhen to use it
Open checkoutOmit optional checkout fieldsThe payer chooses the payment method and enters their own details.
Restricted payment methodSend rail onlyYou 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 preselectionSend rail: "PSE" and pse.bankYou 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 customerSend inlineCustomer onlyYou already collected customer details and want to skip asking for them again.
Restricted and pre-filledSend rail and inlineCustomerYou 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:
Eventdata.statusMeaning
paymentlink.completedCOMPLETEDThe payer completed the payment successfully.
paymentlink.expiredEXPIREDThe 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.
{
  "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"
  }
}
{
  "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
  }
}
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

FieldTypeRequiredDescription
amountTypestringYesThe only value currently supported is FIXED, which means the payer will be charged exactly amountInCents.
amountInCentsnumberYesAmount to collect in cents.
referencestringYesYour internal reference for this payment link. Returned in payment-link webhook events.
callbackUrlstringYesURL to redirect the payer to after the payment flow ends.
railstringNoRestricts 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.
inlineCustomerobjectNoCustomer data to attach to the payin created from this link. If omitted, the payer enters their details in the checkout.
pseobjectNoPSE-specific defaults for the payment link. Only send this object when rail is PSE.
pse.bankstringNoPSE 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.