Skip to main content

Webhook Payload Reference

Every WePay webhook uses the same envelope. What changes is which properties appear inside data, and that depends on the event and on whether the contract uses milestones.

This page focuses on which properties you get, and what they mean. Examples are taken from captured webhook deliveries. reference and metadata are included only when they were supplied at contract creation, so they are absent from these captures.

The envelope​

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "payment.completed",
"createdAt": "2026-09-22T10:15:00Z",
"data": { }
}
PropertyDescription
idUnique ID for this delivery. Use it for idempotency
eventThe event type. Also sent in the X-WePay-Event header
createdAtWhen the webhook was created
dataThe event body, described below

Rule 1 — Missing properties are absent, not null​

WePay omits properties that have no value. A contract without milestones does not send "milestoneId": null — the key is simply not there.

// Contract without milestones — no milestoneId key at all
"data": {
"contractId": "CNT-01K5ZR8M2N4Q",
"status": "Escrow"
}

Always check for presence, not for null.

if ("milestoneId" in event.data) { /* milestone-scoped event */ }
Do not rely on a fixed set of properties

New properties may be added to data over time. Read the properties you need and ignore the rest. Do not fail on unknown properties.


Rule 2 — status means different things​

This is the most important thing to understand.

Contract typedata.status refers todata.contractStatus
Without milestonesthe contractabsent
With milestonesthe milestonepresent, holds the contract status

dispute.created and dispute.resolved are the exception: they omit both status and contractStatus. Use milestoneId only to tell which milestone the dispute belongs to.

On a milestone contract, data.status is the status of that one milestone, not the whole contract.

Side by side​

Same event, two contract types.

Contract without milestones — funded (payment.completed)

"data": {
"contractId": "CNT-01M48FPS2A4R",
"status": "Escrow",
"previousStatus": "Approved",
"amount": 3069,
"currency": "SAR"
}

status is the contract. The contract is funded.

Contract with milestones — one milestone funded (milestone.payment.completed)

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"status": "Escrow",
"previousStatus": "Approved",
"contractStatus": "Escrow",
"contractPreviousStatus": "Approved",
"amount": 634.5
"currency": "SAR"
}

Here status: Escrow describes only that milestone. previousStatus is the milestone's previous status (Pending). The contract itself stays Approved in contractStatus while other milestones remain unpaid.

Do not treat milestone status as contract status

If you read data.status as the contract status, a milestone contract will look fully paid as soon as the first milestone is funded.

  • When contractStatus is present, it is the contract status and status is the milestone
  • When contractStatus is absent, status is the contract status
  • dispute.created and dispute.resolved have neither, even when milestoneId is present
const contractStatus = "contractStatus" in event.data
? event.data.contractStatus
: event.data.status;

Rule 3 — Event names switch too​

For anything that can happen at either level, the event name itself tells you the scope.

Contract-scopedMilestone-scoped
payment.completedmilestone.payment.completed
contract.releasedmilestone.released
contract.disputedmilestone.disputed
contract.disput.resolvedmilestone.disput.resolved
contract.refundedmilestone.refunded
contract.completedmilestone.completed
contract.payment.underpaidmilestone.payment.underpaid
contract.payment.overpaidmilestone.payment.overpaid
refund.partial-initiatedmilestone.refund.partial-initiated
refund.partial-succeededmilestone.refund.partial-succeeded
refund.full-initiatedmilestone.refund.full-initiated
refund.full-succeededmilestone.refund.full-succeeded

Events that are always contract-scoped: contract.created, contract.approved, contract.rejected, contract.cancelled.

dispute.created and dispute.resolved keep the same event name for both scopes. When the dispute is on a milestone, data.milestoneId is present. These two events do not include status, previousStatus, amount, or contractStatus.

The resolved status events are named contract.disput.resolved and milestone.disput.resolved.


Rule 4 — Identifiers are opaque​

Treat contractId, milestoneId, disputeId, paymentId, transactionId, and transactionReference as opaque strings. Do not parse them, infer ordering from them, or assume a length or format. Store them as text. disputeId is a numeric string such as "774", not a prefixed id.


Payloads by event family​

Contract lifecycle​

contract.created · contract.approved · contract.rejected · contract.cancelled · contract.delivered · contract.received · contract.completed

"data": {
"contractId": "CNT-01M48D4SZYAW",
"status": "Approved",
"previousStatus": "Pending",
"amount": 3000,
"currency": "SAR",
"timestamp": "2026-10-06T10:46:27.3587531Z"
}

Always contract-scoped. No milestoneId, no contractStatus. On contract.created, previousStatus is none.

When you supplied a reference or metadata at contract creation, those properties are added to data. They are omitted otherwise.

Payment​

payment.completed · milestone.payment.completed

Milestone payment:

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"status": "Escrow",
"previousStatus": "Pending",
"contractStatus": "Approved",
"contractPreviousStatus": "Approved",
"amount": 1000,
"currency": "SAR",
"paymentId": "07077237544386819574",
"transactionId": "319499",
"transactionReference": "trans_01M48D6850BDQ7N5RH77K35KZF",
"invoiceId": "7237544",
"timestamp": "2026-10-06T10:47:14.507281Z",
"paymentMethod": "PaymentGateway"
}

Contract payment. Drop milestoneId, contractStatus, and contractPreviousStatus:

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

paymentId, transactionId, and invoiceId are opaque strings. transactionReference is the payment reference (trans_...). Use it later to match a release or a succeeded refund to this payment.

paymentMethod is PaymentGateway for online payment and BankTransfer for a bank transfer.

Bank transfer​

bank_transfer.initiated · bank_transfer.buyer_acknowledged

Adds paymentBankInfo, and on acknowledgement buyerAcknowledged and buyerAcknowledgedAt.

See Bank Transfer Events for the full flow.

Payment mismatch​

contract.payment.underpaid · contract.payment.overpaid · payment.unmatched · and the milestone.payment.* equivalents

"data": {
"contractId": "CNT-01K5ZR8M2N4Q",
"status": "XXXX",
"previousStatus": "XXXX",
"currency": "SAR",
"paymentMethod": "BankTransfer",
"amountExpected": 1057.5,
"amountReceived": 1000.0,
"amountDifference": -57.5,
"vIBAN": "SA03****************7519",
"transferReference": "PT2609220001",
"senderName": "Kareem Hasan",
"senderIBAN": "SA03********************1234",
"timestamp": "2026-09-22T11:40:00Z"
}

amountDifference is negative when underpaid and positive when overpaid.

Dispute​

dispute.created · contract.disputed · milestone.disputed · dispute.resolved · contract.disput.resolved · milestone.disput.resolved

These events do not share one shape. disputeId is a string. raisedBy on an API-opened dispute is ThirdParty.

dispute.created — no status, no amount. Includes milestoneId only for a milestone dispute.

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"disputeId": "774",
"raisedBy": "ThirdParty",
"currency": "SAR",
"timestamp": "2026-10-06T10:47:41.6619642Z"
}

milestone.disputed — milestone status stays Escrow. The contract moves to Dispute in contractStatus.

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"disputeId": "774",
"raisedBy": "ThirdParty",
"status": "Escrow",
"previousStatus": "Approved",
"contractStatus": "Dispute",
"contractPreviousStatus": "Approved",
"amount": 1000,
"currency": "SAR",
"timestamp": "2026-10-06T10:47:41.6542281Z"
}

contract.disputed — status is the contract, and it becomes Dispute. No milestoneId.

"data": {
"contractId": "CNT-01M48FPS2A4R",
"disputeId": "777",
"raisedBy": "ThirdParty",
"status": "Dispute",
"previousStatus": "Escrow",
"amount": 3000.00,
"currency": "SAR",
"timestamp": "2026-10-06T11:32:48.2241167Z"
}

dispute.resolved — disputeDecision and comment only. No status, no amount, no raisedBy. disputeDecision is PartialRefund or FullRelease.

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"disputeId": "774",
"disputeDecision": "PartialRefund",
"currency": "SAR",
"comment": "testing",
"timestamp": "2026-10-06T10:49:57.8658008Z"
}

milestone.disput.resolved — status change. amount is 0. The refunded or released amount is on the refund or release webhook. transactionReference is the original payment reference.

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZHR5",
"disputeId": "774",
"disputeDecision": "PartialRefund",
"status": "PartiallyRefunded",
"previousStatus": "Escrow",
"contractStatus": "Approved",
"contractPreviousStatus": "Dispute",
"amount": 0,
"currency": "SAR",
"comment": "testing",
"transactionReference": "trans_01M48D6850BDQ7N5RH77K35KZF",
"timestamp": "2026-10-06T10:49:57.7678822Z"
}

FullRelease uses the same shape with disputeDecision: "FullRelease" and status: "Released".

contract.disput.resolved — same decision fields, without milestone properties. previousStatus is Dispute.

"data": {
"contractId": "CNT-01M48FPS2A4R",
"disputeId": "777",
"disputeDecision": "PartialRefund",
"status": "PartiallyRefunded",
"previousStatus": "Dispute",
"amount": 0,
"currency": "SAR",
"comment": "testing",
"transactionReference": "trans_01M48FR847WMRG9HWQQGYCPPJN",
"timestamp": "2026-10-06T11:34:12.6705123Z"
}

Refund​

refund.full-initiated · refund.full-succeeded · refund.partial-initiated · refund.partial-succeeded · the milestone.refund.* equivalents · refund_request.cancelled · overpaid.refund.succeeded · underpaid.refund.succeeded · unmatched.refund.succeeded

Partial refund initiated while resolving a contract dispute. status is PartiallyRefunded. amount is the amount being refunded. transactionId is the refund id.

"data": {
"contractId": "CNT-01M48FPS2A4R",
"status": "PartiallyRefunded",
"previousStatus": "Dispute",
"amount": 2000,
"currency": "SAR",
"transactionId": "1425",
"timestamp": "2026-10-06T11:34:12.6915178Z"
}

Milestone partial refund initiated. milestoneId is present on data, and repeated under metadata.

"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 is sent when that refund settles. It carries transactionReference (the original payment reference from milestone.payment.completed), not transactionId.

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

On *-initiated, transactionId is the refund identifier. On milestone.refund.partial-succeeded, match the original payment with transactionReference.

Refunds raised against a mismatched payment also include a nested refund object:

"refund": {
"id": 4521,
"amount": 57.5,
"status": "Succeeded"
}

Release​

contract.released · milestone.released

milestone.released after a FullRelease decision. It does not include paymentId, transactionId, or invoiceId. amount is the released amount. transactionReference matches the original payment.

"data": {
"contractId": "CNT-01M48D4SZYAW",
"milestoneId": "MST-01M48D4SZH53",
"status": "Released",
"previousStatus": "Escrow",
"contractStatus": "Approved",
"contractPreviousStatus": "Dispute",
"amount": 1000,
"currency": "SAR",
"transactionReference": "trans_01M48DJXFQ731SF5PZKVHN450B",
"timestamp": "2026-10-06T10:55:14.2792703Z"
}

Property matrix​

PropertyAppears on
contractIdevery event
currencyevery event
timestampevery event
status, previousStatusstatus-changing events. Absent on dispute.created and dispute.resolved
reference, metadatawhen you supplied them at contract creation. metadata.milestoneId is also sent on milestone.refund.partial-initiated
amountmost events. Absent on dispute.created and dispute.resolved. 0 on contract.disput.resolved and milestone.disput.resolved
milestoneIdmilestone-scoped events, including dispute.created and dispute.resolved when the dispute is on a milestone
contractStatus, contractPreviousStatusmilestone payment, disputed, disput-resolved, refund, and release events. Absent on dispute.created and dispute.resolved
paymentId, invoiceIdpayment.completed and milestone.payment.completed
transactionIdpayment completed events, and refund *-initiated (the refund id)
transactionReferencepayment completed, *.disput.resolved, milestone.released, and milestone.refund.partial-succeeded
paymentMethodpayment completed events. Also bank transfer and mismatch events
paymentBankInfobank transfer events
buyerAcknowledged, buyerAcknowledgedAtbank_transfer.buyer_acknowledged
amountExpected, amountReceived, amountDifferencemismatch events
vIBAN, transferReference, senderName, senderIBANmismatch events
refundmismatch refund events and refund_request.cancelled
disputeIddispute events
raisedBydispute.created, contract.disputed, milestone.disputed
disputeDecisiondispute.resolved, contract.disput.resolved, milestone.disput.resolved. Values: PartialRefund, FullRelease
commentthe same resolved dispute events as disputeDecision

What to read, by use case​

You want to knowRead
Is the whole contract paid?contractStatus when it is present, otherwise status. dispute.created and dispute.resolved have neither
Is this one milestone paid?status, on an event where contractStatus is also present
Which milestone does this concern?milestoneId
How did the buyer pay?paymentMethod
Did the money actually arrive?payment.completed or milestone.payment.completed. Never bank_transfer.buyer_acknowledged
Was the amount wrong?amountDifference on a mismatch event
Where do I show transfer details?paymentBankInfo on bank_transfer.initiated
Have I already processed this?id, or the X-WePay-Webhook-Id header
Which of my records is this?reference and metadata, when you supplied them
Who raised the dispute?raisedBy on dispute.created or *.disputed
How was the dispute decided?disputeDecision on dispute.resolved (PartialRefund or FullRelease)
What note was attached to the resolution?comment
Which payment does a release or succeeded refund belong to?transactionReference
How much was refunded or released?amount on the refund or release webhook, not on *.disput.resolved