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 a contract without milestones. status becomes Dispute
dispute.createdDispute has been created. Includes milestoneId when the dispute is on a milestone. No status or amount
dispute.resolvedDispute has been resolved. disputeDecision is PartialRefund or FullRelease, plus comment. No status or amount
contract.disput.resolvedA contract dispute has been resolved. amount is 0. Status becomes PartiallyRefunded or Released
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
refund.partial-initiatedA partial contract refund has been initiated. Status is PartiallyRefunded. transactionId is the refund id
refund.partial-succeededA partial contract refund has settled
contract.completedContract has been fully completed
milestone.payment.completedMilestone payment has been completed. Milestone status is Escrow. contractStatus stays Approved until every milestone is paid
milestone.releasedMilestone has been released. Milestone status is Released. Carries transactionReference and the released amount
milestone.disputedMilestone has been disputed. Milestone status stays Escrow. contractStatus becomes Dispute
milestone.disput.resolvedA milestone dispute has been resolved. Milestone status becomes PartiallyRefunded or Released. amount is 0
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
milestone.refund.partial-initiatedMilestone partial refund has been initiated. Status is PartiallyRefunded. amount is the refunded amount
milestone.refund.partial-succeededMilestone partial refund has settled. Carries transactionReference, not transactionId
webhook.testTest 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​

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​

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​

FieldDescription
idUnique webhook delivery ID
eventEvent type that triggered the webhook
createdAtWebhook creation timestamp
data.contractIdExternal contract ID
data.milestoneIdMilestone ID string, for example MST-01M48D4SZHR5. Present on milestone events, including dispute.created and dispute.resolved when the dispute is on a milestone
data.disputeIdDispute ID as a string, for example "774"
data.raisedByWho opened the dispute. ThirdParty when opened through the API. On dispute.created, contract.disputed, and milestone.disputed
data.disputeDecisionResolution. PartialRefund or FullRelease. On resolved dispute events
data.commentResolution note. On resolved dispute events
data.referenceYour reference from contract creation, if provided
data.statusContract status, or milestone status when contractStatus is also present. Absent on dispute.created and dispute.resolved
data.previousStatusPrevious value of data.status. none on contract.created
data.contractStatusContract status on milestone-scoped status events. Absent on dispute.created and dispute.resolved
data.contractPreviousStatusPrevious contract status on those same milestone events
data.amountEvent 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.currencyCurrency code. Always SAR
data.paymentIdPayment ID on payment.completed and milestone.payment.completed
data.transactionIdPayment transaction ID, or the refund ID on refund *-initiated events
data.transactionReferenceOriginal payment reference (trans_...). On payment completed, *.disput.resolved, milestone.released, and milestone.refund.partial-succeeded
data.invoiceIdInvoice ID on payment completed events
data.timestampEvent timestamp
data.metadataMetadata from contract creation when you supplied it. milestone.refund.partial-initiated also sends metadata.milestoneId
data.paymentMethodHow the buyer paid, for example BankTransfer or PaymentGateway
data.paymentBankInfoAccount details issued for the contract on bank transfer events. Contains iban, accountNumber, bankName, bankCode, beneficiaryName
data.buyerAcknowledgedWhether the buyer confirmed they sent a bank transfer
data.buyerAcknowledgedAtWhen the buyer confirmed they sent a bank transfer
data.amountExpectedExpected amount, on underpaid and overpaid events
data.amountReceivedAmount received so far, on underpaid and overpaid events
data.amountDifferenceamountReceived minus amountExpected. Negative when underpaid
data.vIBANThe contract account that received an incoming transfer
data.transferReferenceBank reference for an incoming transfer
data.senderNameName on the sending bank account
data.senderIBANSending account IBAN, masked
data.refundRefund details, when a refund has been raised
note

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:

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

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 {{BaseUrl}}/apps/api/webhooks.