Webhook Notifications
Overview
Webhooks allow you to receive real-time notifications about contract events. Instead of polling the API for updates, WePay sends HTTP POST requests to your configured endpoint whenever important events occur.
Key Benefits:
- Real-time Updates: Get notified immediately when events occur.
- Reduced API Calls: No need to poll for status changes.
- Reliable Delivery: Automatic retries with exponential backoff.
- Secure: HMAC-SHA256 signature verification.
Webhook Event Types
WePay sends webhooks for the following events:
| Event Type | Description |
|---|---|
| contract.created | A new contract has been created |
| contract.approved | Contract has been approved by the other party |
| contract.rejected | Contract has been rejected |
| contract.cancelled | Contract has been cancelled |
| payment.completed | Payment has been completed and funds are in escrow |
| contract.released | Funds have been released to the seller |
| contract.disputed | A dispute has been raised on a contract without milestones. status becomes Dispute |
| dispute.created | Dispute has been created. Includes milestoneId when the dispute is on a milestone. No status or amount |
| dispute.resolved | Dispute has been resolved. disputeDecision is PartialRefund or FullRelease, plus comment. No status or amount |
| contract.disput.resolved | A contract dispute has been resolved. amount is 0. Status becomes PartiallyRefunded or Released |
| contract.refunded | Contract has been refunded to the buyer. Legacy combined event emitted on the Refunded status transition |
| refund.full-initiated | A full contract refund has been initiated. The contract enters RefundInProgress |
| refund.full-succeeded | A full contract refund has settled with the bank. The contract is now Refunded |
| refund.partial-initiated | A partial contract refund has been initiated. Status is PartiallyRefunded. transactionId is the refund id |
| refund.partial-succeeded | A partial contract refund has settled |
| contract.completed | Contract has been fully completed |
| milestone.payment.completed | Milestone payment has been completed. Milestone status is Escrow. contractStatus stays Approved until every milestone is paid |
| milestone.released | Milestone has been released. Milestone status is Released. Carries transactionReference and the released amount |
| milestone.disputed | Milestone has been disputed. Milestone status stays Escrow. contractStatus becomes Dispute |
| milestone.disput.resolved | A milestone dispute has been resolved. Milestone status becomes PartiallyRefunded or Released. amount is 0 |
| milestone.completed | Milestone has been fully completed. Milestone status is Completed |
| milestone.refund.full-initiated | Milestone full refund has been initiated. Milestone status is RefundInProgress |
| milestone.refund.full-succeeded | Milestone full refund has settled with the bank. Milestone status is Refunded |
| milestone.refund.partial-initiated | Milestone partial refund has been initiated. Status is PartiallyRefunded. amount is the refunded amount |
| milestone.refund.partial-succeeded | Milestone partial refund has settled. Carries transactionReference, not transactionId |
| webhook.test | Test webhook sent from /webhooks/test endpoint |
Test Webhook
Send a test webhook to verify your endpoint.
Endpoint
POST {{BaseUrl}}/apps/api/webhooks/test
Example Request
curl -X POST "{baseUrl}/apps/api/webhooks/test" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Example Response
{
"data": {
"webhookId": "123e4567-e89b-12d3-a456-426614174000",
"message": "Test webhook has been queued for delivery"
},
"message": "Test webhook sent.",
"status": 200,
"validationErrors": []
}
Your endpoint receives a test webhook with event type webhook.test.
View Delivery Logs
View webhook delivery attempts.
Endpoint
GET /apps/api/webhooks/logs?page=1&pageSize=20
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number |
| pageSize | integer | 20 | Number of records per page. Max 100 |
Example Request
curl -X GET "{baseUrl}/apps/api/webhooks/logs?page=1&pageSize=20" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Example Response
{
"data": {
"logs": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"eventType": "payment.completed",
"externalContractId": "CNT-2601-00100068",
"contractId": 12345,
"httpStatusCode": 200,
"isSuccess": true,
"attemptNumber": 1,
"errorMessage": null,
"durationMs": 245,
"createdAt": "2026-01-19T14:30:00Z"
}
],
"totalCount": 150,
"page": 1,
"pageSize": 20
},
"message": "",
"status": 200,
"validationErrors": []
}
Log Fields
| Field | Description |
|---|---|
| id | Unique log entry identifier |
| eventType | Event type sent |
| externalContractId | External contract ID |
| contractId | Internal contract ID |
| httpStatusCode | HTTP status returned by your endpoint |
| isSuccess | Whether delivery was successful |
| attemptNumber | Delivery attempt number |
| errorMessage | Error message if delivery failed |
| durationMs | Request duration in milliseconds |
| createdAt | Delivery attempt timestamp |
Webhook Payload Format
All webhooks are sent as HTTP POST requests with JSON body.
Headers Sent with Each Webhook
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-WePay-Signature | HMAC-SHA256 signature for verification |
| X-WePay-Event | Event type, for example payment.completed |
| X-WePay-Webhook-Id | Unique identifier for this webhook delivery |
| X-WePay-Timestamp | ISO 8601 timestamp when webhook was created |
Webhook Payload Structure
payment.completed for a contract without milestones:
{
"id": "8027c8ea-2f82-4238-889d-20d4788ee55c",
"event": "payment.completed",
"createdAt": "2026-10-06T11:32:01.3736241Z",
"data": {
"contractId": "CNT-01M48FPS2A4R",
"status": "Escrow",
"previousStatus": "Approved",
"amount": 3069,
"currency": "SAR",
"paymentId": "07077237900386852273",
"transactionId": "322683",
"transactionReference": "trans_01M48FR847WMRG9HWQQGYCPPJN",
"invoiceId": "7237900",
"timestamp": "2026-10-06T11:32:01.3714774Z",
"paymentMethod": "PaymentGateway"
}
}
Payload Field Descriptions
| Field | Description |
|---|---|
| id | Unique webhook delivery ID |
| event | Event type that triggered the webhook |
| createdAt | Webhook creation timestamp |
| data.contractId | External contract ID |
| data.milestoneId | Milestone ID string, for example MST-01M48D4SZHR5. Present on milestone events, including dispute.created and dispute.resolved when the dispute is on a milestone |
| data.disputeId | Dispute ID as a string, for example "774" |
| data.raisedBy | Who opened the dispute. ThirdParty when opened through the API. On dispute.created, contract.disputed, and milestone.disputed |
| data.disputeDecision | Resolution. PartialRefund or FullRelease. On resolved dispute events |
| data.comment | Resolution note. On resolved dispute events |
| data.reference | Your reference from contract creation, if provided |
| data.status | Contract status, or milestone status when contractStatus is also present. Absent on dispute.created and dispute.resolved |
| data.previousStatus | Previous value of data.status. none on contract.created |
| data.contractStatus | Contract status on milestone-scoped status events. Absent on dispute.created and dispute.resolved |
| data.contractPreviousStatus | Previous contract status on those same milestone events |
| data.amount | Event amount. The refunded or released amount is on the refund or release webhook. 0 on *.disput.resolved. Absent on dispute.created and dispute.resolved |
| data.currency | Currency code. Always SAR |
| data.paymentId | Payment ID on payment.completed and milestone.payment.completed |
| data.transactionId | Payment transaction ID, or the refund ID on refund *-initiated events |
| data.transactionReference | Original payment reference (trans_...). On payment completed, *.disput.resolved, milestone.released, and milestone.refund.partial-succeeded |
| data.invoiceId | Invoice ID on payment completed events |
| data.timestamp | Event timestamp |
| data.metadata | Metadata from contract creation when you supplied it. milestone.refund.partial-initiated also sends metadata.milestoneId |
| data.paymentMethod | How the buyer paid, for example BankTransfer or PaymentGateway |
| data.paymentBankInfo | Account details issued for the contract on bank transfer events. Contains iban, accountNumber, bankName, bankCode, beneficiaryName |
| data.buyerAcknowledged | Whether the buyer confirmed they sent a bank transfer |
| data.buyerAcknowledgedAt | When the buyer confirmed they sent a bank transfer |
| data.amountExpected | Expected amount, on underpaid and overpaid events |
| data.amountReceived | Amount received so far, on underpaid and overpaid events |
| data.amountDifference | amountReceived minus amountExpected. Negative when underpaid |
| data.vIBAN | The contract account that received an incoming transfer |
| data.transferReference | Bank reference for an incoming transfer |
| data.senderName | Name on the sending bank account |
| data.senderIBAN | Sending account IBAN, masked |
| data.refund | Refund details, when a refund has been raised |
Properties that do not apply to an event are omitted. Do not expect milestoneId, disputeId, paymentId, transactionId, transactionReference, or invoiceId on every event. Dispute payloads are documented in Payload Reference, Create Dispute, and Resolve Dispute.
For bank transfer specific payloads, see Bank Transfer Events.
Refund Event Payloads
Refund operations fire two webhooks for each refund:
- An
*-initiatedevent when the refund request is accepted. - An
*-succeededevent when the refund has settled with the bank.
On *-initiated, transactionId is the refund identifier.
On milestone.refund.partial-succeeded, transactionReference is the original payment reference from milestone.payment.completed. That succeeded event does not include transactionId.
For milestone refunds, the milestone id is data.milestoneId. milestone.refund.partial-initiated also repeats it as data.metadata.milestoneId.
refund.full-initiated
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "refund.full-initiated",
"createdAt": "2026-04-14T14:30:00Z",
"data": {
"contractId": "CNT-2604-00100002",
"reference": "your-reference-123",
"status": "RefundInProgress",
"previousStatus": "Escrow",
"amount": 1000.0,
"currency": "SAR",
"transactionId": "4521",
"timestamp": "2026-04-14T14:30:00Z",
"metadata": {
"metadata1": "your-custom-data-1",
"reference": "your-reference-123"
}
}
}
refund.full-succeeded
{
"id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"event": "refund.full-succeeded",
"createdAt": "2026-04-14T15:05:00Z",
"data": {
"contractId": "CNT-2604-00100002",
"reference": "your-reference-123",
"status": "Refunded",
"previousStatus": "RefundInProgress",
"amount": 1000.0,
"currency": "SAR",
"transactionId": "4521",
"timestamp": "2026-04-14T15:05:00Z",
"metadata": {
"reference": "your-reference-123"
}
}
}
Partial refund from a dispute
A partial refund raised while resolving a dispute uses status PartiallyRefunded. amount is the amount refunded.
refund.partial-initiated (contract without milestones):
{
"id": "a01842d1-444d-4b01-ab4d-01a670965e05",
"event": "refund.partial-initiated",
"createdAt": "2026-10-06T11:34:12.6957216Z",
"data": {
"contractId": "CNT-01M48FPS2A4R",
"status": "PartiallyRefunded",
"previousStatus": "Dispute",
"amount": 2000,
"currency": "SAR",
"transactionId": "1425",
"timestamp": "2026-10-06T11:34:12.6915178Z"
}
}
milestone.refund.partial-initiated:
{
"id": "80f3e99a-34c6-4dae-a232-ac3ae212facd",
"event": "milestone.refund.partial-initiated",
"createdAt": "2026-10-06T10:49:57.9502074Z",
"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"status": "PartiallyRefunded",
"previousStatus": "Escrow",
"contractStatus": "Approved",
"contractPreviousStatus": "Dispute",
"amount": 400,
"currency": "SAR",
"transactionId": "1424",
"timestamp": "2026-10-06T10:49:57.9475973Z",
"metadata": {
"milestoneId": "MST-01M48D4SZHR5"
}
}
}
milestone.refund.partial-succeeded:
{
"id": "6b55471a-53d7-4a81-a879-5477c9a196e4",
"event": "milestone.refund.partial-succeeded",
"createdAt": "2026-10-06T10:53:15.5610125Z",
"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"status": "PartiallyRefunded",
"previousStatus": "Escrow",
"contractStatus": "Approved",
"contractPreviousStatus": "Dispute",
"amount": 400.00,
"currency": "SAR",
"transactionReference": "trans_01M48D6850BDQ7N5RH77K35KZF",
"timestamp": "2026-10-06T10:53:15.5574041Z"
}
}
Retry Policy
If your endpoint does not respond with a 2xx status code, WePay retries delivery with exponential backoff.
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 30 seconds |
| 3rd retry | 2 minutes |
| 4th retry | 10 minutes |
| 5th retry | 1 hour |
After 5 failed retries, delivery is marked as failed.
Retryable Status Codes
These status codes will trigger retries:
- 408 Request Timeout
- 429 Too Many Requests
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
Non-Retryable Status Codes
These status codes will not trigger retries (considered permanent failures):
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
Auto-Disable Policy
After 10 consecutive failed deliveries, your webhook subscription will be automatically disabled. To re-enable:
- Fix your endpoint issue.
- Update the webhook URL using
POST {{BaseUrl}}/apps/api/webhooks.