Refund Contract Funds
After a contract has reached the Escrow status, or a Dispute has been raised, you can return funds to the buyer using the refund API.
Webhook events for refunds are documented in Webhook Notifications.
Unified Refund Endpointβ
All refund operations are handled by a single endpoint. The request body determines whether the refund targets the whole contract or a specific milestone.
POST /apps/api/contracts/{externalContractId}/refund
Path Parameters
| Field | Type | Description | Required / Notes / Example |
|---|---|---|---|
| externalContractId | string | The external contract identifier | Required Example: CNT-2604-00100002 |
Headers
- Authorization:
Bearer {access_token} - Content-Type: application/json
Refund Request Bodyβ
| Field Name | Type | Description | Required / Notes |
|---|---|---|---|
| milestoneId | integer | When provided, the refund targets a specific milestone | Required if contract has milestones |
| reason | string | Mandatory reason for the refund, kept for audit trail | Required Max 500 chars |
The buyer's IBAN and account holder name are not part of the refund request. WePay resolves them automatically from the verified buyer record.
Buyer Eligibility and IBAN Resolutionβ
Before any refund is created, WePay runs a pre-flight check against the buyer record:
- Authorization: The buyer must have completed the WePay authorization step.
- Bank account verification: When the platform requires a verified IBAN for the buyer, the buyer must have a verified bank account.
- IBAN resolution: Once eligibility passes, WePay resolves the buyer IBAN and account holder name from the verified record.
If buyer authorization or bank verification is missing, WePay rejects the refund and sends an onboarding SMS to the buyer when possible.
| Condition | HTTP | Message key |
|---|---|---|
| Buyer has not completed authorization | 400 | BuyerNotAuthorized |
| Buyer's bank account is not verified | 400 | BuyerBankAccountNotVerified |
| Buyer record not found for this business | 404 | ExternalUserNotFound |
| IBAN could not be resolved from the verified record | 500 | FailedToResolveIban |
Refund Preconditionsβ
| Scope | Required contract status | Required milestone status |
|---|---|---|
| Contract refund | Escrow or Dispute | -- |
| Milestone refund | Escrow or Dispute | Escrow |
- Only one active refund of a given type is allowed per target. Any additional refund request of the same type is rejected.
- When a refund request is created, the contract transitions to
RefundInProgress. - If the refund is milestone-scoped, the milestone also transitions to
RefundInProgress. - When the refund succeeds, the final status becomes
Refunded. But if the refund targets a milestone, the contract status will beCancelled.
Refund β Contractβ
Returns the entire escrowed contract principal to the buyer. WePay platform fees and taxes are retained.
Example Requestβ
curl --location 'https://api.wepay.com.sa/apps/api/contracts/CNT-2604-00100002/refund' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"milestoneId": 9,
"reason": "Order cancelled by buyer; goods never shipped."
}'
Example Responseβ
{
"data": {
"refundId": 4521,
"settlementId": null,
"milestoneId": null,
"refundedAmount": 1000.00,
"releasedToSellerAmount": null,
"type": "FullRefund",
"externalContractId": "CNT-2604-00100002"
},
"message": "RefundCreatedSuccessfully",
"status": 200,
"validationErrors": []
}
Full Refund β Milestoneβ
Returns the entire escrowed amount of a single milestone to the buyer.
Example Requestβ
curl --location 'https://api.wepay.com.sa/apps/api/contracts/CNT-2604-00100002/refund' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"milestoneId": 13,
"reason": "Milestone deliverable rejected; refund agreed."
}'
Example Responseβ
{
"data": {
"refundId": 4523,
"settlementId": null,
"milestoneId": 13,
"refundedAmount": 400.00,
"releasedToSellerAmount": null,
"type": "FullRefund",
"externalContractId": "CNT-2604-00100002"
},
"message": "RefundCreatedSuccessfully",
"status": 200,
"validationErrors": []
}
Refund Response Fieldsβ
All refund operations return the same unified response.
| Field | Type | Description |
|---|---|---|
| refundId | integer | Internal WePay refund identifier. Use it to correlate with refund webhooks |
| settlementId | integer / null | Settlement identifier for seller release portion. Only for partial refunds |
| milestoneId | integer / null | Refunded milestone ID. Only for milestone-scoped refunds |
| refundedAmount | number | Amount returned to buyer |
| releasedToSellerAmount | number / null | Net amount released to seller. Only for partial refunds |
| type | string | "FullRefund" |
| externalContractId | string | External contract id |
Refund Status Transitionsβ
| Event | Contract status | Milestone status (if applicable) |
|---|---|---|
| Refund initiated | RefundInProgress | RefundInProgress |
HTTP Status Codesβ
| Status | Description |
|---|---|
| 200 | Refund initiated successfully |
| 400 | Validation failed, invalid status, invalid amount, active refund already exists, buyer not authorized, or buyer bank account not verified |
| 401 | Unauthorized |
| 403 | Caller is not authorized to refund this contract |
| 404 | Contract, milestone, or buyer record not found |
| 409 | Concurrent refund race condition detected |
| 500 | Buyer IBAN could not be resolved from the verified record |