Skip to main content

Journey 3: Expire a payment request

You prove that when a checkout is not paid in time, the payer sees that it has expired, your webhook receives payment_request.expired, and your app closes the order or offers a new payment request.

Payment links never expire

Only HOSTED_PAGE and EMBEDDED requests have an expiresAt. A PAYMENT_LINK request never expires and never sends payment_request.expired.

You need

  • Before you start done.
  • A new payment request with "executionMode": "HOSTED_PAGE" or "EMBEDDED". Check that the create response has expiresAt. Set cancelRedirectUrl so the payer can return to you.

How expiry works

In production a hosted or embedded request lives for 60 minutes. The payer can start a payment for the first 45 minutes. The last 15 minutes are the closing window: no new payment can start, but a payment already sent can finish. In sandbox, simulate-expiry moves these clocks so you do not wait.

Step 1: Open checkout in your app

Open the paymentUrl in a browser and stay on the payment method screen. You do not need to start a payment.

Step 2: Expire the request

Send POST /v1/collections/simulate-expiry:

{
"paymentRequestReference": "ORDER-10045",
"target": "PAYMENT_REQUEST"
}

Expect paymentRequestStatus: "EXPIRED" and paymentRequestExpiresAt in the past. Your webhook receives payment_request.expired with amountPaid: 0.

Step 3: Prove it in your app

Refresh the hosted page. It shows the payer that the request is no longer open. If you set cancelRedirectUrl, the page returns the payer there after a short countdown, with a button to go at once. Arriving at cancelRedirectUrl is not proof of expiry. Confirm from the webhook or the API before acting.

CheckExpected
GET /v1/collections/payment-requests/{id}status: "EXPIRED"
Your webhook URLpayment_request.expired with amountPaid and currency
Your appCloses the order, or creates a new payment request and shows its checkout

Variations

The closing window

Test the point where checkout stops new payments but a started payment can still finish. Send simulate-expiry with "target": "PAYMENT_DEADLINE" instead. Expect newPaymentsDeadline in the past and paymentRequestExpiresAt still in the future. While the request stays open, no expiry webhook is sent. If the payer never started a payment, this same call can already make the request EXPIRED, and then payment_request.expired is sent at once. Always read paymentRequestStatus in the response.

  • Hosted page: a payer who has not started a payment sees that the request no longer accepts new payments, with a Return to merchant button.
  • Embedded: your page receives hubpay:expired for the closed state. The request is still open for up to 15 minutes, so do not create a new payment request until the webhook or the API says EXPIRED. Otherwise you may collect twice.

If the request is still open, send "target": "PAYMENT_REQUEST" to finish.

Expired after a part payment

Run Journey 2 step 1 with a crypto tab, then send "target": "PAYMENT_REQUEST". Expect EXPIRED and payment_request.expired with amountPaid of the part received. The money is not lost: expire the quote with "target": "CRYPTO_QUOTE", then send simulate-payment-completion with the saved paymentId. Expect payment.completed while the request stays EXPIRED.

A deposit is still confirming at expiry

In production a crypto transfer sent in time but not yet confirmed holds the request open past expiresAt. To test it, use the crypto no-wallet path: send simulate-crypto-deposit with "status": "PENDING", then simulate-expiry with "target": "PAYMENT_REQUEST". The response shows paymentRequestExpiresAt in the past but the status still UNPAID, and no expired webhook is sent. Then send the deposit again with the same depositId and "status": "CONFIRMED". The request becomes PAID and your webhook receives payment_request.paid.

If it does not work

ProblemCauseFix
400 on simulate-expiryThe request is a PAYMENT_LINK or has no expiresAtCreate a new request with HOSTED_PAGE or EMBEDDED
400 on simulate-expiryThe request is already PAID or CANCELLEDA paid or cancelled request cannot expire
Status is not EXPIREDA deposit is still PENDINGSee the variation above, or confirm the deposit
No hubpay:expired in the embedded pageThe checkout was not set up with a valid origin, or your page is not served over HTTPSSee Embedded checkout

Good to know

  • An expired request never reopens. Sending the call again changes nothing.
  • Neither expiry target changes the crypto quote's own timer. CRYPTO_QUOTE is a separate target used in Journey 2.
  • The production timing rules are in Payment request events.