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.deliveredSeller marked the contract as delivered
contract.receivedBuyer confirmed receipt of delivery
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 refund, contract or milestone, has been initiated. The contract enters RefundInProgress
refund.full-succeededA full refund has settled with the bank. The contract or milestone is now Refunded
refund.partial-initiatedA partial refund, contract or milestone, has been initiated. The contract enters RefundInProgress
refund.partial-succeededA partial refund has settled. The contract or milestone is now Refunded
contract.completedContract has been fully completed
webhook.testTest webhook sent from /webhooks/test endpoint

Configure Webhook Subscription​

Before receiving webhooks, register your webhook URL.

Endpoint​

POST /apps/api/webhooks

Headers​

Authorization: Bearer {access_token}
Content-Type: application/json

Request Body​

{
"webhookUrl": "https://yourdomain.com/webhooks/wepay"
}

Field Descriptions​

FieldTypeRequiredDescription
webhookUrlstringYesPublic HTTPS endpoint URL that receives webhook notifications. Example: https://api.yoursite.com/webhooks/wepay

Example Request​

curl -X POST "{baseUrl}/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://yourdomain.com/webhooks/wepay"
}'

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://yourdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "Webhook created successfully.",
"status": 200,
"validationErrors": []
}

Response Fields​

FieldDescription
idBusiness entity ID used as webhook subscription identifier
webhookUrlRegistered webhook endpoint
secretKeySecret key for signature verification. Save it securely
isActiveWhether the webhook subscription is active
consecutiveFailuresNumber of consecutive delivery failures. Resets on success
lastSuccessAtTimestamp of last successful delivery
lastFailureAtTimestamp of last failed delivery
createdAtCreation timestamp
important

Save the secretKey securely. You need it to verify webhook signatures. The secret is only shown when creating or regenerating it. Never expose it in client-side code.


Get Webhook Subscription​

Retrieve current webhook configuration.

Endpoint​

GET /apps/api/webhooks

Headers​

Authorization: Bearer {access_token}

Example Request​

curl -X GET "{baseUrl}/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://yourdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": "2026-01-19T14:30:00Z",
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "",
"status": 200,
"validationErrors": []
}

Update Webhook URL​

Update the existing webhook subscription with a new URL.

Endpoint​

POST /apps/api/webhooks

Request Body​

{
"webhookUrl": "https://newdomain.com/webhooks/wepay"
}

Example Request​

curl -X POST "{baseUrl}/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://newdomain.com/webhooks/wepay"
}'

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://newdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": "2026-01-19T14:30:00Z",
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "Webhook updated successfully.",
"status": 200,
"validationErrors": []
}
note

Updating the webhook URL also re-enables the subscription if it was disabled due to consecutive failures.


Regenerate Secret Key​

Regenerate the secret key immediately if it is compromised.

Endpoint​

POST /apps/api/webhooks/regenerate-secret

Example Request​

curl -X POST "{baseUrl}/apps/api/webhooks/regenerate-secret" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": {
"secretKey": "whsec_xYz123AbCdEfGhIjKlMnOpQrStUvWxYz456"
},
"message": "Webhook secret regenerated successfully.",
"status": 200,
"validationErrors": []
}
important

After regenerating, update your server immediately to use the new secret. Webhooks signed with the old key will fail verification.


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

Delete Webhook Subscription​

Remove your webhook subscription to stop receiving notifications.

Endpoint​

DELETE /apps/api/webhooks

Example Request​

curl -X DELETE "{baseUrl}/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": true,
"message": "Webhook deleted successfully.",
"status": 200,
"validationErrors": []
}

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",
"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.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 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"
}
}
}

refund.partial-initiated​

{
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"event": "refund.partial-initiated",
"createdAt": "2026-04-14T14:32:00Z",
"data": {
"contractId": "CNT-2604-00100002",
"reference": "your-reference-123",
"status": "RefundInProgress",
"previousStatus": "Escrow",
"amount": 250.0,
"currency": "SAR",
"transactionId": "4522",
"timestamp": "2026-04-14T14:32:00Z",
"metadata": {
"reference": "your-reference-123"
}
}
}
note

data.amount on partial refund events is the buyer refund amount, not the seller release amount.

refund.partial-succeeded​

{
"id": "d4e5f6a7-b8c9-0123-def0-456789012345",
"event": "refund.partial-succeeded",
"createdAt": "2026-04-14T15:10:00Z",
"data": {
"contractId": "CNT-2604-00100002",
"reference": "your-reference-123",
"status": "Refunded",
"previousStatus": "RefundInProgress",
"amount": 250.0,
"currency": "SAR",
"transactionId": "4522",
"timestamp": "2026-04-14T15:10:00Z",
"metadata": {
"reference": "your-reference-123"
}
}
}

Milestone-level Refund Metadata​

Milestone-level refunds use the same refund event types as contract-level refunds. The affected milestone is identified by data.metadata.milestoneId.

{
"event": "refund.full-initiated",
"data": {
"contractId": "CNT-2604-00100002",
"status": "RefundInProgress",
"previousStatus": "Escrow",
"amount": 400.0,
"currency": "SAR",
"transactionId": "4523",
"metadata": {
"milestoneId": "13",
"reference": "your-reference-123"
}
}
}

Verifying Webhook Signatures​

Verify the webhook signature before processing any webhook.

Signature Format​

X-WePay-Signature contains:

sha256={hex_signature}

Verification Process​

  1. Extract the signature from X-WePay-Signature.
  2. Read the raw JSON request body.
  3. Compute HMAC-SHA256 using your webhook secret key.
  4. Compare the computed signature with the received signature using constant-time comparison.

Node.js Example​

const crypto = require("crypto")

function verifyWebhookSignature(payload, signature, secretKey) {
const expectedSignature =
"sha256=" +
crypto.createHmac("sha256", secretKey).update(payload, "utf8").digest("hex")

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
)
}

app.post("/webhooks/wepay", (req, res) => {
const signature = req.headers["x-wepay-signature"]
const payload = JSON.stringify(req.body)

if (!verifyWebhookSignature(payload, signature, YOUR_SECRET_KEY)) {
return res.status(401).send("Invalid signature")
}

const event = req.body

switch (event.event) {
case "payment.completed":
// handle payment
break
case "contract.released":
// handle release
break
case "refund.full-initiated":
case "refund.full-succeeded":
case "refund.partial-initiated":
case "refund.partial-succeeded":
// handle refund events
break
}

res.status(200).send("OK")
})

PHP Example​

<?php
function verifyWebhookSignature($payload, $signature, $secretKey) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secretKey);
return hash_equals($expectedSignature, $signature);
}

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEPAY_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($payload, $signature, YOUR_SECRET_KEY)) {
http_response_code(401);
exit('Invalid signature');
}

$event = json_decode($payload, true);

switch ($event['event']) {
case 'payment.completed':
break;
case 'contract.released':
break;
case 'refund.full-initiated':
case 'refund.full-succeeded':
case 'refund.partial-initiated':
case 'refund.partial-succeeded':
break;
}

http_response_code(200);
echo 'OK';
?>

Python Example​

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
SECRET_KEY = 'your_webhook_secret_key'

def verify_signature(payload, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

@app.route('/webhooks/wepay', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-WePay-Signature', '')
payload = request.get_data(as_text=True)

if not verify_signature(payload, signature, SECRET_KEY):
abort(401)

event = request.get_json()

if event['event'] == 'payment.completed':
pass
elif event['event'] == 'contract.released':
pass
elif event['event'] in [
'refund.full-initiated',
'refund.full-succeeded',
'refund.partial-initiated',
'refund.partial-succeeded'
]:
pass

return 'OK', 200

C# Example​

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public class WebhookController : ControllerBase
{
private readonly string _secretKey = "your_webhook_secret_key";

private bool VerifyWebhookSignature(string payload, string signature)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();

return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}

[HttpPost("webhooks/wepay")]
public async Task<IActionResult> HandleWebhook()
{
var signature = Request.Headers["X-WePay-Signature"].ToString();

using var reader = new StreamReader(Request.Body);
var payload = await reader.ReadToEndAsync();

if (!VerifyWebhookSignature(payload, signature))
{
return Unauthorized();
}

var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(payload);

switch (webhookEvent?.Event)
{
case "payment.completed":
break;
case "contract.released":
break;
case "refund.full-initiated":
case "refund.full-succeeded":
case "refund.partial-initiated":
case "refund.partial-succeeded":
break;
}

return Ok();
}
}
important

Always use constant-time comparison (timingSafeEqual, hash_equals, compare_digest, FixedTimeEquals) to prevent timing attacks.


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.

Webhook Best Practices​

1. Respond Quickly​

  • Return 200 OK as soon as you receive the webhook.
  • Process the event asynchronously if needed.
  • Timeout is 30 seconds. Respond faster to avoid retries.

2. Handle Duplicates​

  • Webhooks may be delivered more than once.
  • Use X-WePay-Webhook-Id to deduplicate.
  • Make processing idempotent.

3. Verify Signatures​

  • Always verify X-WePay-Signature before processing.
  • Reject unsigned or incorrectly signed requests.
  • Use constant-time comparison.

4. Use HTTPS​

  • Production webhook URLs must use HTTPS.
  • Keep SSL certificates valid and renewed.

5. Log Everything​

  • Log received webhooks.
  • Log signature verification results.
  • Keep logs for troubleshooting failed deliveries.

6. Monitor Your Endpoint​

  • Check delivery logs through /apps/api/webhooks/logs.
  • Set alerts for consecutive failures.
  • Test periodically using /apps/api/webhooks/test.

Webhook Error Handling​

ErrorSolution
400 Invalid URLEnsure URL is valid and uses HTTP or HTTPS
401 UnauthorizedCheck access token validity
404 Not FoundCreate webhook subscription first

Example Error Response​

{
"message": "Invalid webhook URL",
"status": 400,
"validationErrors": [
{
"field": "webhookUrl",
"message": "URL must be a valid HTTP or HTTPS URL"
}
]
}

Complete Webhook Integration Flow​

  1. Register Webhook URL β€” Call POST /apps/api/webhooks and save the secretKey securely.
  2. Implement Webhook Handler β€” Create endpoint at your webhookUrl, implement signature verification, handle different event types.
  3. Test Your Integration β€” Call POST /apps/api/webhooks/test, verify your endpoint receives the test webhook, check signature verification works.
  4. Go Live β€” Create contracts via API and monitor delivery logs for any failures.
  5. Ongoing Maintenance β€” Check /apps/api/webhooks/logs periodically, regenerate secret if compromised, update URL if endpoint changes.

FAQ​

Q: How do I know if my webhook subscription is working?

A: Use the test endpoint (POST /apps/api/webhooks/test) and check delivery logs via GET /apps/api/webhooks/logs.

Q: My webhook was disabled. How do I re-enable it?

A: Update your webhook URL via POST /apps/api/webhooks. This resets the failure counter and re-enables the subscription.

Q: Can I have multiple webhook URLs?

A: No, each business can have only one webhook subscription. All events are sent to the same URL.

Q: What if I miss a webhook?

A: You can always query the contract status via the API. Webhooks are for real-time updates, but the API is the source of truth.

Q: Is the webhook payload the same for all events?

A: The structure is the same, but some fields (like paymentId, transactionId) are only present for relevant events (e.g., payment.completed).

Q: How long are webhook delivery logs retained?

A: WePay retains delivery logs accessible via the API. We recommend keeping your own logs for at least 90 days.

Q: What happens if my server is down when a webhook is sent?

A: WePay will retry delivery up to 5 times with exponential backoff (10s, 30s, 2min, 10min, 1hr). If all retries fail, check your delivery logs when your server is back up. | 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.delivered | Seller marked the contract as delivered | | contract.received | Buyer confirmed receipt of delivery | | 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 | | contract.completed | Contract has been fully completed | | webhook.test | Test webhook (sent via /webhooks/test endpoint) |


Configure Webhook Subscription​

Before receiving webhooks, you need to register your webhook URL.

Endpoint​

POST /apps/api/webhooks

Headers:

Authorization: Bearer `access_token`
Content-Type: application/json

Request Body​

{
"webhookUrl": "https://yourdomain.com/webhooks/wepay"
}

Field Descriptions​

FieldTypeRequired?Description
webhookUrlStringYesYour HTTPS endpoint URL to receive webhook notifications. Must be publicly accessible. Example: https://api.yoursite.com/webhooks

Example Request (cURL)​

curl -X POST "https://api.wepay.com.sa/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://yourdomain.com/webhooks/wepay"
}'

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://yourdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": null,
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "Webhook created successfully.",
"status": 200,
"validationErrors": []
}

Response Fields​

FieldDescription
idYour business entity ID (used as webhook subscription identifier)
webhookUrlYour registered webhook endpoint
secretKeySecret key for signature verification (Save this securely)
isActiveWhether the webhook is currently active (false if disabled due to failures)
consecutiveFailuresNumber of consecutive delivery failures (resets to 0 on success)
lastSuccessAtTimestamp of last successful delivery
lastFailureAtTimestamp of last failed delivery
createdAtWhen the business entity was created

Important: Save the secretKey securely. You'll need it to verify webhook signatures. The secret is only shown once when creating or regenerating. Never expose your secret key in client-side code.


Get Webhook Subscription​

Retrieve your current webhook configuration.

Endpoint​

GET /apps/api/webhooks

Headers:

Authorization: Bearer `access_token`

Example Request (cURL)​

curl -X GET "https://api.wepay.com.sa/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://yourdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": "2026-01-19T14:30:00Z",
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "",
"status": 200,
"validationErrors": []
}

Update Webhook URL​

Update your existing webhook subscription with a new URL.

Endpoint​

POST /apps/api/webhooks

Headers:

Authorization: Bearer `access_token`
Content-Type: application/json

Request Body​

{
"webhookUrl": "https://newdomain.com/webhooks/wepay"
}

Example Request (cURL)​

curl -X POST "https://api.wepay.com.sa/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://newdomain.com/webhooks/wepay"
}'

Example Response​

{
"data": {
"id": "019bb692-28e0-7ae1-926f-59b12c3b784c",
"webhookUrl": "https://newdomain.com/webhooks/wepay",
"secretKey": "whsec_abc123xyz789defghijklmnopqrstuvwxyz",
"isActive": true,
"consecutiveFailures": 0,
"lastSuccessAt": "2026-01-19T14:30:00Z",
"lastFailureAt": null,
"createdAt": "2026-01-19T12:00:00Z"
},
"message": "Webhook updated successfully.",
"status": 200,
"validationErrors": []
}

Note: Updating the webhook URL also re-enables the subscription if it was disabled due to consecutive failures.


Regenerate Secret Key​

If your secret key is compromised, regenerate it immediately.

Endpoint​

POST /apps/api/webhooks/regenerate-secret

Headers:

Authorization: Bearer `access_token`

Example Request (cURL)​

curl -X POST "https://api.wepay.com.sa/apps/api/webhooks/regenerate-secret" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": {
"secretKey": "whsec_xYz123AbCdEfGhIjKlMnOpQrStUvWxYz456"
},
"message": "Webhook secret regenerated successfully.",
"status": 200,
"validationErrors": []
}

Important: After regenerating, update your server immediately to use the new secret key. Webhooks signed with the old key will fail verification.


feature/docs-revamp-v1.5.0:apps/docs/docs/Integration/webhook.md

Test Webhook​

Send a test webhook to verify your endpoint is working correctly.

Endpoint​

POST /apps/api/webhooks/test

Headers:

Authorization: Bearer {access_token}

Example Request (cURL)​

curl -X POST "https://api.wepay.com.sa/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 will receive a test webhook with event type webhook.test. This is a special event type used only for testing and will not be triggered by actual contract operations.


View Delivery Logs​

View the history of webhook delivery attempts.

Endpoint​

GET /apps/api/webhooks/logs?page=1&pageSize=20

Headers:

Authorization: Bearer {access_token}

Query Parameters​

ParameterTypeDefaultDescription
pageInteger1Page number for pagination
pageSizeInteger20Number of records per page (max 100)

Example Request (cURL)​

curl -X GET "https://api.wepay.com.sa/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 (UUID format)
eventTypeType of event that was sent
externalContractIdExternal contract ID (e.g., "CNT-2601-00100068")
contractIdInternal contract ID (integer)
httpStatusCodeHTTP status code returned by your endpoint (null if connection failed)
isSuccessWhether delivery was successful (2xx response)
attemptNumberWhich attempt this was (1 = first attempt, up to 6 with retries)
errorMessageError message if delivery failed (null on success)
durationMsHow long the request took in milliseconds
createdAtWhen the delivery attempt was made

Delete Webhook Subscription​

Remove your webhook subscription to stop receiving notifications.

Endpoint​

DELETE /apps/api/webhooks

Headers:

Authorization: Bearer {access_token}

Example Request (cURL)​

curl -X DELETE "https://api.wepay.com.sa/apps/api/webhooks" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Example Response​

{
"data": true,
"message": "Webhook deleted successfully.",
"status": 200,
"validationErrors": []
}

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-EventThe event type (e.g., "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",
"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 this webhook
createdAtWhen the webhook was created
data.contractIdExternal contract ID (e.g., "CNT-2601-00100068")
data.referenceYour reference from contract creation (if provided)
data.statusCurrent contract status
data.previousStatusPrevious contract status
data.amountContract amount in SAR
data.currencyCurrency code (always "SAR")
data.paymentIdPayment ID (for payment events)
data.transactionIdTransaction ID (for payment events)
data.invoiceIdInvoice ID (for payment events)
data.timestampEvent timestamp
data.metadataYour metadata from contract creation (metadata1-4, reference)

Note: Some fields like paymentId, transactionId, and invoiceId are only present for relevant events (e.g., payment.completed).


Verifying Webhook Signatures​

To ensure webhooks are genuinely from WePay, verify the signature.

Signature Format​

The X-WePay-Signature header contains: sha256={hex_signature}

Verification Process​

  1. Extract the signature from X-WePay-Signature header
  2. Get the raw JSON body of the request
  3. Compute HMAC-SHA256 of the body using your secret key
  4. Compare your computed signature with the received signature

Example Implementation (Node.js)​

const crypto = require("crypto")

function verifyWebhookSignature(payload, signature, secretKey) {
const expectedSignature =
"sha256=" +
crypto.createHmac("sha256", secretKey).update(payload, "utf8").digest("hex")

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
)
}

// In your webhook handler:
app.post("/webhooks/wepay", (req, res) => {
const signature = req.headers["x-wepay-signature"]
const payload = JSON.stringify(req.body)

if (!verifyWebhookSignature(payload, signature, YOUR_SECRET_KEY)) {
return res.status(401).send("Invalid signature")
}

// Process the webhook
const event = req.body
console.log("Received event:", event.event)

switch (event.event) {
case "payment.completed":
// Handle payment completed
break
case "contract.released":
// Handle contract released
break
}

// Always respond with 200 OK quickly
res.status(200).send("OK")
})

Example Implementation (PHP)​

<?php
function verifyWebhookSignature($payload, $signature, $secretKey) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secretKey);
return hash_equals($expectedSignature, $signature);
}

// In your webhook handler:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEPAY_SIGNATURE'] ?? '';

if (!verifyWebhookSignature($payload, $signature, YOUR_SECRET_KEY)) {
http_response_code(401);
exit('Invalid signature');
}

$event = json_decode($payload, true);

switch($event['event']) {
case 'payment.completed':
// Handle payment completed
break;
case 'contract.released':
// Handle contract released
break;
}

http_response_code(200);
echo 'OK';
?>

Example Implementation (Python)​

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
SECRET_KEY = 'your_webhook_secret_key'

def verify_signature(payload, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

@app.route('/webhooks/wepay', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-WePay-Signature', '')
payload = request.get_data(as_text=True)

if not verify_signature(payload, signature, SECRET_KEY):
abort(401)

event = request.get_json()

if event['event'] == 'payment.completed':
# Handle payment completed
pass
elif event['event'] == 'contract.released':
# Handle contract released
pass

return 'OK', 200

Example Implementation (C#)​

using System.Security.Cryptography;
using System.Text;

public class WebhookController : ControllerBase
{
private readonly string _secretKey = "your_webhook_secret_key";

private bool VerifyWebhookSignature(string payload, string signature)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}

[HttpPost("webhooks/wepay")]
public async Task<IActionResult> HandleWebhook()
{
var signature = Request.Headers["X-WePay-Signature"].ToString();
using var reader = new StreamReader(Request.Body);
var payload = await reader.ReadToEndAsync();

if (!VerifyWebhookSignature(payload, signature))
{
return Unauthorized();
}

var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(payload);

switch (webhookEvent.Event)
{
case "payment.completed":
// Handle payment completed
break;
case "contract.released":
// Handle contract released
break;
}

return Ok();
}
}

Important: Always use constant-time comparison (timingSafeEqual, hash_equals, compare_digest, FixedTimeEquals) to prevent timing attacks.


Retry Policy​

If your endpoint fails to respond with a 2xx status code, WePay will retry delivery with exponential backoff:

AttemptDelay
1st retry10 seconds
2nd retry30 seconds
3rd retry2 minutes
4th retry10 minutes
5th retry1 hour

After 5 failed retries, the webhook 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 prevent unnecessary retries. To re-enable:

  1. Fix your endpoint issues
  2. Update the webhook URL via POST /apps/api/webhooks