Payment request events
Payment request events use the v1.collection.payment_request.* prefix. The current lifecycle events are created, part_paid, paid, cancelled and expired.
Overview
Payment Requests let you collect money via a pre-generated link. Whenever a request moves through its life-cycle we emit a webhook so your system can react in real-time (update UI, trigger fulfilment, etc.).
Event catalogue
| Event | When it triggers |
|---|---|
v1.collection.payment_request.created | The payment request is first generated |
v1.collection.payment_request.part_paid | A partial payment is captured but the payable balance > 0 |
v1.collection.payment_request.paid | The request balance reaches 0 from payments in RECEIVED or COMPLETE |
v1.collection.payment_request.cancelled | The request is cancelled by the user or after a payment is rejected |
v1.collection.payment_request.expired | A HOSTED_PAGE or EMBEDDED request closed without being paid in full; see Expiry below for when this happens |
Event flow
payment_request.paid is an aggregate balance signal: it means the requested balance is covered even while an individual payment remains RECEIVED. For crypto, use the individual payment.completed event and confirm its API status is COMPLETE. A later crypto compliance rejection can cancel a previously paid or part-paid request. See Payment lifecycle and webhooks.
Additional data
payment_request.part_paid, payment_request.paid and payment_request.expired include the amount received so far in the data object:
{
"amountPaid": "400.00",
"currency": "AED"
}
| Field | Meaning |
|---|---|
amountPaid | Cumulative amount received against the payment request so far, as a decimal string |
currency | Currency of amountPaid, always the payment request's currency |
payment_request.created and payment_request.cancelled have an empty data object. The payment request id is relatedObject.id on every event.
Use these fields to update your records from the event. Before taking a business action such as fulfilment, confirm the latest state with Get payment request. See Event format for the webhook envelope.
Expiry
HOSTED_PAGE and EMBEDDED payment requests carry an expiresAt set to 60 minutes after creation. PAYMENT_LINK requests have no expiresAt and do not expire.
- Up to 45 minutes after creation (
expiresAtminus 15 minutes) the payer can start a payment. A crypto quote taken during this time runs until that 45 minute mark, however late it was taken. - Between 45 minutes and
expiresAtno new payment can start. This window lets funds already sent arrive and confirm. - The request becomes
EXPIRED, andpayment_request.expiredis sent withamountPaidandcurrency, at the earlier of two points: the 45 minute mark, if the payer never took a quote or started a card or bank payment; otherwiseexpiresAt, if the request is stillUNPAIDorPART_PAIDand no deposit sent in time is still confirming. A confirming deposit holds the request open pastexpiresAt: the request becomesPAIDif the deposit covers the balance, and expires once nothing is confirming otherwise.expiresAtalways reports the 60 minute instant, so an early expiry arrives before it. - A request that reached
PAIDis unaffected and receives no expiry event.
Worked example
A payment request is created at 10:00, so expiresAt is 11:00 and the last moment a payment can be started is 10:45. Time the payer spends before reaching the quote screen comes off the quote window rather than extending the request:
| Payer's journey | What happens |
|---|---|
| Reaches the quote screen at 10:15 | The quote shown is valid until 10:45, a 30 minute window |
| Reaches the quote screen at 10:40 | The quote shown is valid until 10:45, a 5 minute window |
| Sends crypto at 10:44, confirmed on-chain at 10:52 | Inside the arrival window, so the payment is processed normally and the request becomes PART_PAID or PAID |
| Has not taken a quote or started a card or bank payment by 10:45 | There is no longer time to take a quote and pay, so the request becomes EXPIRED at 10:45 and payment_request.expired is sent. A payer who took a quote at 10:40 and sent funds at 10:44 is not in this case: their request waits until 11:00, and longer while the deposit is visible but unconfirmed |
| Took a quote but never sent funds | The request becomes EXPIRED at 11:00 and payment_request.expired is sent with amountPaid of zero |
| Paid in full at 10:30 | The request is PAID; nothing expires and no expiry event is sent |
If your own system also times the request out, key it off the same moment of creation so the payer can never be mid-checkout with a valid quote on screen while your side has already written the request off, and treat payment_request.expired or the API status, not the clock alone, as the signal to close it: a deposit still confirming can hold a request open past expiresAt and then pay it.
An expired request does not reopen. Funds that arrive after expiry are not lost: depending on the payment method they are either credited to your Hubpay wallet and shown against the payment request in the API, or held by Hubpay for review, in which case our operations team will contact you to complete or refund the payment.