Journey 4: Pay out to a beneficiary
You prove that when a payment request with payout details is paid, Hubpay creates a payout to your beneficiary, your webhook receives every payout event, and your system treats the money as delivered only when the payout is PAID.
You need
- Before you start done.
- Payouts are available for
AEDpayment requests paying a beneficiary with a UAE bank account.
In production two things happen outside your control: a checker in your company may have to approve the payout in the Hubpay portal, and the bank reports whether it was paid. In sandbox you play both roles with one endpoint.
Step 1: Create a beneficiary
Send POST /v1/beneficiaries. The smallest valid body for a UAE business, with your own values in the placeholders:
{
"externalId": "<YOUR_BENEFICIARY_REFERENCE>",
"personalDetails": {
"type": "BUSINESS",
"name": "<BENEFICIARY_LEGAL_NAME>",
"addressLine1": "<BENEFICIARY_ADDRESS_LINE_1>",
"country": "AE"
},
"bankDetails": {
"accountNumber": "<UAE_IBAN>",
"bic": "<BANK_BIC>",
"currency": "AED",
"bankCountry": "AE"
}
}
Save the id from the response. A 409 means a beneficiary with that account number already exists: use the existing one. See Create beneficiary.
Step 2: Create the payment request with payout details
Create an AED payment request as in Journey 1, and add:
"payoutDetails": {
"payoutBeneficiaryId": "<beneficiary id from step 1>",
"payoutReference": "<YOUR_PAYOUT_REFERENCE>",
"payoutPurposeOfPayment": "PROPERTY_RENTAL"
}
payoutPurposeOfPayment takes one value from the list in the API reference. PROPERTY_RENTAL, PROPERTY_PURCHASE, CONSTRUCTION_EXPENSES and SERVICE_CHARGES are the common ones for property payments.
Step 3: Pay the request
The quickest way is one card simulation. Send POST /v1/collections/simulate-payment:
{
"paymentRequestReference": "ORDER-10045",
"paymentMethod": "CARD"
}
Save the paymentId from the response. The card payment completes in this call and Hubpay creates the payout at once. One sandbox difference: a simulated card payment does not add to your sandbox wallet balance, so do not check the balance in this journey. Crypto works too, and does credit the wallet: the payout is created when you complete the payment in Journey 1, step 2.
Do not use BANK_TRANSFER for this journey. A simulated bank transfer creates the payout and sends its webhook, but sandbox cannot attach that payout to the payment, so step 4 onwards cannot find it.
Step 4: Approve the payout, if your account needs it
Read the payout with Get payment for payment request and look at its status.
-
INITIATED: your account has no second-approver rule. Skip to step 5. -
PENDING_APPROVAL: your account has maker-checker on. In production a checker approves in the Hubpay portal. In sandbox, sendPOST /v1/collections/simulate-payout:{"paymentRequestReference": "ORDER-10045","paymentId": "<paymentId from step 3>","outcome": "APPROVED"}Expect
payoutStatus: "INITIATED"and apayout.initiatedwebhook. To test the other branch, send"outcome": "REJECTED"instead, with an optionalfailureReason. The payout becomesFAILEDand your webhook receivespayout.failed.
Step 5: Send the bank's result yourself
In production the bank reports whether the payout was paid or failed. In sandbox there is no bank behind the payout, so nothing arrives on its own and the payout stays INITIATED until you act. You send the result. POST /v1/collections/simulate-payout with "outcome": "PAID":
{
"paymentRequestReference": "ORDER-10045",
"paymentId": "<paymentId from step 3>",
"outcome": "PAID"
}
To test the failure path instead, use "outcome": "FAILED" with "failureReason": "Beneficiary account closed".
PENDING_APPROVAL?PAID and FAILED work only after the payout is INITIATED. If you skipped step 4, the call returns 400 with a message that says the payout is waiting for approval and to send APPROVED first. Do step 4, then send this call again.
outcome | Payout becomes | Webhook | Your app should |
|---|---|---|---|
PAID | PAID | payout.paid | Mark the money as delivered to the beneficiary |
FAILED | FAILED | payout.failed | Show the money as received by you but not delivered, and tell someone |
Step 6: Prove it
| Check | Expected |
|---|---|
| Get payment for payment request | payout status: "PAID" or "FAILED" |
| Your webhook URL, no maker-checker | payment.completed, payout.initiated, then payout.paid or payout.failed |
Your webhook URL, maker-checker and APPROVED | payment.completed, payout.pending_approval, payout.initiated, then payout.paid or payout.failed |
Your webhook URL, maker-checker and REJECTED | payment.completed, payout.pending_approval, then payout.failed. No payout.initiated is sent |
| Your app | Treats the payout as delivered only on PAID. payment.completed means the payment finished on your side, not that the beneficiary was paid |
Each simulate-payout call moves the payout one step. A step that does not fit the current status is refused with a 400 that names the status, so PAID on a payout still awaiting approval tells you to send APPROVED first. Repeating an outcome the payout already reached returns alreadyProcessed: true and no webhook, which is how you test that your system handles duplicates.
If it does not work
| Problem | Cause | Fix |
|---|---|---|
| No payout after the payment completes | The request is not AED, has no payoutDetails, or the beneficiary's bank is not in the UAE | Create the request again with valid payout details |
| Payout webhook arrived but the payment shows no payout | You paid with a simulated BANK_TRANSFER, which cannot be linked to its payout in sandbox | The request is already PAID, so create a new AED request with payoutDetails and CARD or CRYPTO in paymentMethods, then repeat from step 2 |
404 on simulate-payout | The paymentId is not on this request, or the payment is not COMPLETE yet | Complete the payment first, then use its paymentId |
400 on simulate-payout | The outcome does not fit the payout's current status | Read the status in the message and send the step it allows |
Payout stays INITIATED however long you wait | Expected. No bank is connected in sandbox, so nothing moves a payout on its own | Send PAID or FAILED in step 5 |
Good to know
- A payout failure never undoes the payment. In production the money stays in your Hubpay wallet and the payout can be retried by your operations team.
- A part-paid request creates one payout per completed payment, each for that payment's amount. Move each one separately.
- The API returns payout statuses in uppercase. Webhook names stay lowercase.