On this page
Payloads
Read the versioned webhook envelope and the complete invoice, withdrawal or payment-channel deposit snapshot in data.
Every webhook body is a versioned envelope. The data object is the same merchant-facing resource contract returned by the corresponding status API, so your handler can apply a complete snapshot instead of merging partial deltas. That holds for all three resources — invoices, withdrawals and payment-channel deposits share one envelope and one payload rule.
Envelope
| Field | Type | Contract |
|---|---|---|
event_id |
string | Stable prefixed ID (evt_…), identical to X-Webhook-Id and unchanged across retries and replay |
event_type |
string | One value from the event catalog |
version |
integer | Payload schema version. The current version is 1 |
occurred_at |
Unix seconds | Time of the resource transition; unlike the delivery timestamp, it does not change on retry |
data |
object | Full invoice, withdrawal or payment-channel deposit snapshot for this event |
event_id is a delivery identity: one endpoint's copy of one transition. Two endpoints subscribed to the same event receive two different evt_ values for the same payment. The business identity is the resource id inside data — inv_…, wdr_… or pcd_… — and it is identical across endpoints, transitions and replays. Deduplicate money on the business id; deduplicate processing attempts on event_id.
Store event_id, event_type, version, and the raw body before starting expensive work. Reject a schema version your integration does not support rather than guessing its shape.
Invoice payload
Example: invoice.paid.
{
"event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
"event_type": "invoice.paid",
"version": 1,
"occurred_at": 1739281200,
"data": {
"invoice_id": "inv_5CcyDYmMUGtzYL10q0Iimr",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"status": "paid",
"is_final": true,
"is_test": false,
"payment_url": "https://checkout.paymos.io/invoice/inv_5CcyDYmMUGtzYL10q0Iimr",
"order": {
"external_id": "order-12345",
"client_id": "customer-67890",
"amount": "100.00",
"currency": "USD"
},
"payment": {
"currency": "USDT",
"network": "TRC20",
"chain_id": 728126428,
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"expected": "50.00",
"address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"exchange_rate": "2.00",
"paid": "50.00",
"remaining": "0",
"fee": "0.50",
"net": "49.50",
"transfers": [
{
"tx_hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"amount": "50.00",
"status": "confirmed",
"created_at": 1739281020,
"confirmed_at": 1739281200,
"required_confirmations": 19,
"estimated_confirmation_at": 1739281080,
"explorer_url": "https://tronscan.org/#/transaction/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
},
"expires_at": 1739284200,
"completed_at": 1739281200,
"created_at": 1739277600,
"updated_at": 1739281200
}
}
Invoice data follows the Get Invoice response. Fields that do not exist yet for the current state are null or omitted by the JSON contract; use status and is_final as the state authority.
Withdrawal payload
Example: withdrawal.completed.
{
"event_id": "evt_CbYeOgBp9Kt2U3j74fxxLl",
"event_type": "withdrawal.completed",
"version": 1,
"occurred_at": 1739281200,
"data": {
"withdrawal_id": "wdr_7K2M9P4Q8R1X5Z3A0bC2dE",
"external_order_id": "payout-12345",
"status": "completed",
"is_final": true,
"is_test": false,
"amount": "25.00",
"fee": "0.50",
"currency": "USDT",
"network": "TRC20",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"tx_hash": "7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
"explorer_url": "https://tronscan.org/#/transaction/7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
"created_at": 1739277600,
"completed_at": 1739281200
}
}
Withdrawal data follows the Get Withdrawal response. tx_hash and explorer_url are populated when a tracked on-chain transaction is available; they are not a substitute for status.
Payment channel deposit payload
Example: payment_channel.deposit.confirmed.
{
"event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
"event_type": "payment_channel.deposit.confirmed",
"version": 1,
"occurred_at": 1767225930,
"data": {
"id": "pcd_8ScRvL4jNq2XkB7mTfZdWu",
"payment_channel_id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
"project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
"payment_channel_external_id": "customer-42",
"status": "confirmed",
"is_final": true,
"is_test": false,
"currency": "USDT",
"network": "TRC20",
"chain_id": 728126428,
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"gross": "100",
"fee": "1",
"net": "99",
"applied_fee_percent": 1.0,
"customer_fee_percent": 0,
"tx_hash": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"transfer_id": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25:41a614f803b6fd780986a42c78ec9c7f77e6ded13c:41c9f6a2b7d0138e54ca3b91f6072ed48a5c1e93b7:0",
"source_address": "TW9s4RkAqBnLpVdX2ChYzUeGm7QfKt3NbZ",
"destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
"block_height": 68421905,
"first_included_block_timestamp": 1767225900,
"explorer_url": "https://tronscan.org/#/transaction/9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
"created_at": 1767225870,
"updated_at": 1767225930,
"confirmed_at": 1767225930
}
}
Deposit data follows the Retrieve Deposit response. status and is_final are the state authority; applied_fee_percent and customer_fee_percent are the rates frozen for that deposit and do not move when your pricing changes.
The three deposit events carry the same object, so a confirming payload already tells you the channel, the payer identifier, the amount and the chain evidence. confirming and reorged are advisory and may arrive out of order — never let one of them regress a deposit you already know is confirmed.
Compatibility
- Additive fields may appear without a version change. Ignore fields your handler does not use.
- Existing field meanings do not change within one payload version.
- Treat identifiers and decimal amounts as strings. Do not parse prefixed IDs as UUIDs or money as binary floating point.
- Verify the signature against the raw bytes before parsing JSON. Re-serializing the object changes the signed input.