Simulating Payments
When integrating with Hubpay's collections API, you need to verify that your system correctly handles payment lifecycle events — status transitions, webhook notifications, and payment confirmations. In production, this requires real bank transfers or cryptocurrency transactions. In sandbox, you can simulate the entire payment flow with a single API call.
This tutorial walks you through creating a payment request, simulating a payment, and verifying the results — giving you confidence that your integration handles real payments correctly before going live.
Prerequisites
Before you begin, make sure you have:
- A sandbox API key and the ability to authenticate (see Authentication)
- A webhook endpoint registered to receive events (see Testing Webhooks)
- An existing payment request in
UNPAIDstatus — see Create Payment Request for details
If you haven't set up your sandbox environment yet, start with the Getting Started guide.
What the simulation does
The simulate endpoint triggers the same lifecycle as a real payment:
Bank transfer simulation:
- Payment reported as sent →
payment.pendingwebhook - Funds received →
payment.receivedwebhook - Payment request status updated →
payment_request.paidwebhook - Funds settled to wallet →
payment.completedwebhook
Crypto simulation:
- Deposit detected →
payment.pendingwebhook - Deposit confirmed on-chain →
payment.receivedwebhook - Payment request status updated →
payment_request.paid(orpart_paid) webhook - Settlement complete →
payment.completedwebhook (only when fully paid)
Card simulation:
- Payment authorised →
payment.pendingwebhook - Payment captured →
payment.receivedwebhook - Payment request status updated →
payment_request.paidwebhook - Funds settled to wallet →
payment.completedwebhook
Your webhook handler receives the same events in the same order as production — making sandbox simulation a reliable way to validate your integration.
Step 1: Create a payment request
Before you can simulate a payment, you need a payment request in UNPAID status. Create one using the Create Payment Request endpoint. You can include payer details inline or reference an existing payer — see the API reference for all available options.
Make sure to enable the payment method you want to simulate (BANK_TRANSFER, CRYPTO, and/or CARD) in the paymentMethods array.
Note the id from the response — you'll use this as the paymentRequestId in the next step.
Step 2: Simulate a payment
Use the Simulate Payment endpoint with your payment request ID and chosen payment method. If no amount is specified, the full remaining balance is used.
Bank transfer example:
{
"paymentRequestId": "d4f7a8b2-1234-5678-9abc-def012345678",
"paymentMethod": "BANK_TRANSFER"
}
Crypto example:
{
"paymentRequestId": "d4f7a8b2-1234-5678-9abc-def012345678",
"paymentMethod": "CRYPTO",
"cryptoCurrency": "USDT"
}
Card example:
{
"paymentRequestId": "d4f7a8b2-1234-5678-9abc-def012345678",
"paymentMethod": "CARD"
}
The response confirms the simulation succeeded and includes the simulated payment ID, reference, and updated payment request status. See the API reference for the full request and response schema.
The payment request is now fully paid. Your webhook endpoint should have received events in sequence — see the webhook tables below for the exact events per method.
Step 3: Verify the results
Check the payment request status
Query the payment request to confirm the status update:
curl https://sandbox-api.hubpay.io/v1/collections/payment-requests/d4f7a8b2-1234-5678-9abc-def012345678 \
-H "Authorization: Bearer <YOUR_TOKEN>"
Response:
{
"id": "d4f7a8b2-1234-5678-9abc-def012345678",
"status": "PAID",
"amount": 1000.00,
"amountPaid": 1000.00,
"amountRemaining": 0,
"payments": [
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "COMPLETE",
"sendAmount": 1000.00,
"sendCurrency": "AED",
"receiveAmount": 1000.00,
"receiveCurrency": "AED",
"method": "BANK_TRANSFER"
}
]
}
Verify webhook delivery
Check your webhook endpoint. You should see events delivered in this order:
Bank transfer:
| # | Event | Description |
|---|---|---|
| 1 | v1.collection.payment_request.payment.pending | Payment has been initiated |
| 2 | v1.collection.payment_request.payment.received | Funds received |
| 3 | v1.collection.payment_request.paid | Payment request fully paid |
| 4 | v1.collection.payment_request.payment.completed | Funds settled to wallet |
Crypto:
| # | Event | Description |
|---|---|---|
| 1 | v1.collection.payment_request.payment.pending | Deposit detected |
| 2 | v1.collection.payment_request.payment.received | Deposit confirmed on-chain |
| 3 | v1.collection.payment_request.paid | Payment request fully paid |
| 4 | v1.collection.payment_request.payment.completed | Settlement complete |
Card:
| # | Event | Description |
|---|---|---|
| 1 | v1.collection.payment_request.payment.pending | Payment authorised |
| 2 | v1.collection.payment_request.payment.received | Payment captured |
| 3 | v1.collection.payment_request.paid | Payment request fully paid |
| 4 | v1.collection.payment_request.payment.completed | Funds settled to wallet |
Step 4: Test underpayments and multiple payments
Hubpay does not offer customers the option to choose a partial payment amount — the hosted payment page always presents the full invoice amount. However, in practice you have no control over how much a customer actually sends via bank transfer, or how much cryptocurrency arrives on-chain. A customer might send less than the invoiced amount, or split a payment across multiple transactions.
Your integration should handle these scenarios, and the simulation endpoint lets you test them by specifying an amount less than the remaining balance:
curl -X POST https://sandbox-api.hubpay.io/v1/collections/simulate-payment \
-H "Authorization: Bearer <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"paymentRequestId": "d4f7a8b2-1234-5678-9abc-def012345678",
"paymentMethod": "BANK_TRANSFER",
"amount": 400.00
}'
Response:
{
"paymentRequestId": "d4f7a8b2-...",
"paymentId": "...",
"paymentMethod": "BANK_TRANSFER",
"amount": 400.00,
"currency": "AED",
"paymentRequestStatus": "PART_PAID"
}
The payment request status is PART_PAID. You can simulate additional payments until the full amount is reached. Each simulation represents a separate payment from the customer — for example, two bank transfers of 400 AED and 600 AED against a 1,000 AED invoice.
In production, a customer might:
- Send a bank transfer for a different amount than invoiced
- Deposit less cryptocurrency than required (e.g., due to network fees)
- Make multiple smaller payments over time
Your system should listen for payment_request.part_paid webhooks and decide how to handle partial fulfilment — whether that's sending a reminder, adjusting the invoice, or waiting for the remainder.
Crypto payments work differently from bank transfers when underpaid. When a customer deposits less than the invoiced amount:
- The deposit is confirmed on-chain and a
payment.receivedwebhook is sent - The payment request moves to
PART_PAIDand apayment_request.part_paidwebhook is sent - No settlement occurs yet — the customer has until the quote window expires to send the remaining amount
- If the full amount is received before expiry, all deposits are settled together — triggering
payment.completed, wallet balance update, and auto-payout - If the quote window expires with only a partial amount received, the invoice auto-settles with whatever has been received at that point
In production, if the quote window expires with a partial amount, the invoice auto-settles whatever was received — meaning you can receive payment.completed and payout webhooks for less than the full invoiced amount. Your integration should handle this scenario.
In the sandbox simulation, settlement only triggers when the total simulated amount reaches the full invoice value. To test the complete webhook lifecycle including payment.completed and payout, simulate payments that add up to the full amount. Partial crypto simulations will only produce payment.received and payment_request.part_paid events until then.
Step 5: Test with auto-payout
If your payment request includes payout details (a linked beneficiary), the simulation will automatically create a payout to that beneficiary after settlement — just like production.
To test this, create a payment request with payoutDetails including a beneficiaryId, reference, and purposeOfPayment. See the Payouts guide for details on how auto-payout works and how to set up beneficiaries.
Then simulate the payment. Once settlement completes, the payout is created automatically.
Auto-payout is only supported for AED payment requests with a valid UAE beneficiary.
Simulation rules
| Rule | Detail |
|---|---|
| Sandbox only | This endpoint is not available in production |
| One method per request | You cannot mix payment methods (bank transfer, crypto, card) on the same payment request |
| UNPAID or PART_PAID | The payment request must not already be fully paid or cancelled |
| Amount limit | If specified, amount must not exceed the remaining balance |
| AED only | Payouts are only created for AED currency payment requests |
Request reference
POST /v1/collections/simulate-payment
| Field | Type | Required | Description |
|---|---|---|---|
paymentRequestId | UUID | Yes | The payment request ID from POST /v1/collections/payment-requests |
paymentMethod | String | Yes | BANK_TRANSFER, CRYPTO, or CARD |
amount | Number | No | Amount to simulate. Defaults to the full remaining balance |
cryptoCurrency | String | No | USDT or USDC. Defaults to USDT. Only used with CRYPTO |
Response
| Field | Type | Description |
|---|---|---|
paymentRequestId | UUID | The payment request ID |
paymentId | UUID | The simulated payment ID |
paymentReference | String | Unique reference for the simulated payment |
paymentMethod | String | The payment method used |
amount | Number | The amount simulated |
currency | String | The payment currency |
paymentRequestStatus | String | Updated status: PAID or PART_PAID |
Next steps
- Confirming payments — best practices for verifying payment status
- Webhook events — full event reference and payload schemas
- Payouts — understanding the payout lifecycle