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 the contract |
| 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 |
| contract.completed | Contract has been fully completed |
| milestone.payment.completed | Milestone payment has been completed. Milestone status is Escrow |
| milestone.released | Milestone has been released. Milestone status is Released |
| milestone.disputed | Milestone has been disputed. Milestone status is Disputed |
| 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 |
| webhook.test | Test webhook sent from /webhooks/test endpoint |
Test Webhookβ
Send a test webhook to verify your endpoint.
Endpointβ
POST /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β
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "payment.completed",
"createdAt": "2026-01-19T14:30:00Z",
"data": {
"contractId": "CNT-2601-00100068",
"milestoneId": 5442,
"disputeId": 669,
"reference": "your-reference-123",
"status": "Escrow",
"previousStatus": "Approved",
"amount": 600.0,
"currency": "SAR",
"paymentId": "PAY-123456",
"transactionId": "TXN-789012",
"invoiceId": "INV-345678",
"timestamp": "2026-01-19T14:30:00Z",
"metadata": {
"metadata1": "your-custom-data-1",
"metadata2": "your-custom-data-2",
"metadata3": "your-custom-data-3",
"metadata4": "your-custom-data-4",
"reference": "your-reference-123"
}
}
}
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 |
| data.disputeId | Dispute ID |
| data.reference | Your reference from contract creation, if provided |
| data.status | Current contract status |
| data.previousStatus | Previous contract status |
| data.amount | Event amount in SAR |
| data.currency | Currency code. Always SAR |
| data.paymentId | Payment ID for payment events |
| data.transactionId | Transaction ID for payment events, or refund ID for refund events |
| data.invoiceId | Invoice ID for payment events |
| data.timestamp | Event timestamp |
| data.metadata | Metadata from contract creation and event-specific metadata |
Some fields like milestoneId, disputeId, paymentId, transactionId, and invoiceId are only present for relevant 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.
Use data.transactionId, which carries the WePay refund identifier, to correlate initiated and succeeded events.
For milestone-level refunds, the affected milestone is identified using 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"
}
}
}
Milestone-level Refund Metadataβ
Milestone-level refunds use a similar refund event types. The affected milestone is identified by data.milestoneId.
{
"event": "milestone.refund.full-initiated",
"data": {
"contractId": "CNT-2604-00100002",
"milestoneId": 13,
"status": "RefundInProgress",
"previousStatus": "Escrow",
"amount": 400.0,
"currency": "SAR",
"transactionId": "4523",
"metadata": {
"milestoneId": "13",
"reference": "your-reference-123"
}
}
}
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 /apps/api/webhooks.