Journey 1: Complete a payment
You prove that a full payment moves the request to PAID, the payment to COMPLETE, your webhook receives every lifecycle event, and Hubpay creates the payout if the request has payout details.
You need
- Before you start done.
- A new payment request in
UNPAIDstatus. Put the method you will test inpaymentMethods. To also test the payout, create it inAEDwithpayoutDetails. See Payment request payouts.
Step 1: Pay
Pick the tab that matches how your payers pay.
- Bank transfer
- Card
- Crypto, no wallet
- Crypto, test wallet
Send POST /v1/collections/simulate-payment:
{
"paymentRequestReference": "ORDER-10045",
"paymentMethod": "BANK_TRANSFER"
}
The response has paymentRequestStatus: "PAID". Hubpay also completes the payment and credits your wallet in the same call. Skip step 2.
Send POST /v1/collections/simulate-payment:
{
"paymentRequestReference": "ORDER-10045",
"paymentMethod": "CARD"
}
The response has paymentRequestStatus: "PAID". Hubpay also completes the payment in the same call and sends payment.completed. Skip step 2. Note: a simulated card payment does not add to your sandbox wallet balance. Bank transfer and crypto simulations do.
To test a declined card, or a 3-D Secure challenge (the extra identity check some banks ask the cardholder to complete), pay on the hosted page with sandbox test card numbers instead. Ask your Hubpay contact for the test card list. The simulator does not replicate those.
This path uses the real checkout quote, but Hubpay pretends the payer sent the tokens.
-
Open the
paymentUrl. Select Cryptocurrency, then a token and network, until the deposit address is shown. This creates the quote. Do not send anything. -
Send
POST /v1/collections/simulate-crypto-deposit. Generate a new UUID fordepositId. Leave out the amount fields to pay the whole quote.{"paymentRequestReference": "ORDER-10045","depositId": "<NEW_UUID>","status": "CONFIRMED"} -
Save the
paymentIdfrom the response. ExpectdepositStatus: "CONFIRMED",paymentStatus: "RECEIVED"andpaymentRequestStatus: "PAID".
Optional: show the waiting state first. Send the same body with "status": "PENDING" before the CONFIRMED call, keeping the same depositId. The response is depositStatus: "DETECTED", paymentStatus: "PENDING", the request stays UNPAID, and no webhook is sent. This is what your app sees while a real transfer waits for the blockchain. Then send CONFIRMED.
This path sends real test tokens on the Ethereum Sepolia test network. First follow Set up a test crypto wallet and have Hubpay sandbox USDC and some Sepolia ETH in it.
- Open the
paymentUrlin MetaMask's in-app browser on mobile, or in the desktop browser with the MetaMask extension. - Select Cryptocurrency, then USDC and Sepolia. Connect MetaMask. Check MetaMask still shows Sepolia.
- Send the exact amount shown by Hubpay.
- Wait for the blockchain to confirm. Checkout shows the confirmation. Your webhook receives
payment.pending, thenpayment.received, thenpayment_request.paid. - Send
GET /v1/collections/payment-requests/{id}and save theidof the entry inpayments. You need it aspaymentIdin step 2.
While the transfer is still confirming, the payment is PENDING and the request is UNPAID. This is the waiting state your app should show.
Step 2: Complete the crypto payment
Bank transfer and card are completed in step 1. For both crypto tabs, the money is received but not yet in your wallet. In production the provider settles first. In sandbox you ask for it:
Send POST /v1/collections/simulate-payment-completion:
{
"paymentRequestReference": "ORDER-10045",
"paymentId": "<paymentId from step 1>"
}
Expect paymentStatus: "COMPLETE" and paymentRequestStatus: "PAID". Your webhook receives payment.completed.
If the response is 400 because a compliance check is still running, wait a moment and send it again.
Step 3: Prove it
| Check | Expected |
|---|---|
GET /v1/collections/payment-requests/{id} | status: "PAID", amountRemaining: 0, one payment with status: "COMPLETE" |
| Your webhook URL | payment.pending, payment.received, payment_request.paid, payment.completed, in roughly that order |
| Your app | Shows the order as paid. Fulfil it only after your webhook receives payment.completed with a valid signature and the API reports the payment as COMPLETE. |
Step 4: Check the payout
Only if the request was created with payoutDetails.
Read the payout with Get payment for payment request. Expect the payout status to be INITIATED, or PENDING_APPROVAL if your account requires a second approver. The API returns statuses in uppercase. Webhook names stay lowercase. Sandbox cannot move a payout to PAID or FAILED yet.
If it does not work
| Problem | Cause | Fix |
|---|---|---|
400 on simulate-payment | The request is already PAID, or the method is not in paymentMethods | Create a new request with the right method |
400 on simulate-crypto-deposit | No open quote | Open checkout and select a token and network first |
400 on completion | A compliance check is running, or a deposit is still PENDING | Wait, or confirm the pending deposit, then retry |
404 | Wrong reference, or the request belongs to another account | Check the reference and the AccountId header |
| No payout webhook | The request is not AED, has no payoutDetails, or the beneficiary is not a UAE beneficiary | Create the request again with valid payout details |
Good to know
- Send the same deposit or completion call again to test duplicates. You get
alreadyProcessed: trueoralreadyCompleted: trueand no extra webhooks. - Sending
simulate-paymentagain after a full payment is rejected with400, because the request is alreadyPAID. While a request is stillUNPAIDorPART_PAID, each call creates a separate payment. - A payout failure never undoes the wallet credit.