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 bank transfer simulation. Send POST /v1/collections/simulate-payment:
{
"paymentRequestReference": "ORDER-10045",
"paymentMethod": "BANK_TRANSFER"
}
Save the paymentId from the response. Bank transfer and card payments complete in this call, and Hubpay creates the payout at once. For crypto, the payout is created when you complete the payment in Journey 1, step 2.
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: Let the bank pay, or fail
Send 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".
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 money reached your wallet, not the beneficiary |
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 |
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 and you did nothing | Expected. Nothing moves a sandbox payout on its own | Send PAID or FAILED |
Good to know
- A payout failure never undoes the wallet credit. The money stays in your Hubpay wallet, and the payout can be retried by your operations team in production.
- 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.