Skip to main content

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 TypeDescription
contract.createdA new contract has been created
contract.approvedContract has been approved by the other party
contract.rejectedContract has been rejected
contract.cancelledContract has been cancelled
payment.completedPayment has been completed and funds are in escrow
contract.releasedFunds have been released to the seller
contract.disputedA dispute has been raised on the contract
contract.refundedContract has been refunded to the buyer. Legacy combined event emitted on the Refunded status transition
refund.full-initiatedA full contract refund has been initiated. The contract enters RefundInProgress
refund.full-succeededA full contract refund has settled with the bank. The contract is now Refunded
contract.completedContract has been fully completed
milestone.payment.completedMilestone payment has been completed. Milestone status is Escrow
milestone.releasedMilestone has been released. Milestone status is Released
milestone.disputedMilestone has been disputed. Milestone status is Disputed
milestone.completedMilestone has been fully completed. Milestone status is Completed
milestone.refund.full-initiatedMilestone full refund has been initiated. Milestone status is RefundInProgress
milestone.refund.full-succeededMilestone full refund has settled with the bank. Milestone status is Refunded
webhook.testTest 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​

ParameterTypeDefaultDescription
pageinteger1Page number
pageSizeinteger20Number 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​

FieldDescription
idUnique log entry identifier
eventTypeEvent type sent
externalContractIdExternal contract ID
contractIdInternal contract ID
httpStatusCodeHTTP status returned by your endpoint
isSuccessWhether delivery was successful
attemptNumberDelivery attempt number
errorMessageError message if delivery failed
durationMsRequest duration in milliseconds
createdAtDelivery attempt timestamp

Webhook Payload Format​

All webhooks are sent as HTTP POST requests with JSON body.

Headers Sent with Each Webhook​

HeaderDescription
Content-Typeapplication/json
X-WePay-SignatureHMAC-SHA256 signature for verification
X-WePay-EventEvent type, for example payment.completed
X-WePay-Webhook-IdUnique identifier for this webhook delivery
X-WePay-TimestampISO 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​

FieldDescription
idUnique webhook delivery ID
eventEvent type that triggered the webhook
createdAtWebhook creation timestamp
data.contractIdExternal contract ID
data.milestoneIdMilestone ID
data.disputeIdDispute ID
data.referenceYour reference from contract creation, if provided
data.statusCurrent contract status
data.previousStatusPrevious contract status
data.amountEvent amount in SAR
data.currencyCurrency code. Always SAR
data.paymentIdPayment ID for payment events
data.transactionIdTransaction ID for payment events, or refund ID for refund events
data.invoiceIdInvoice ID for payment events
data.timestampEvent timestamp
data.metadataMetadata from contract creation and event-specific metadata
note

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:

  1. An *-initiated event when the refund request is accepted.
  2. An *-succeeded event 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.

AttemptDelay
1st retry10 seconds
2nd retry30 seconds
3rd retry2 minutes
4th retry10 minutes
5th retry1 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:

  1. Fix your endpoint issue.
  2. Update the webhook URL using POST /apps/api/webhooks.