Receive signed HTTP notifications when payments, refunds, invoices, subscriptions and other resources change state.
Configuration
In the developer console, open your API app and add webhook endpoints: an HTTPS URL, the events it receives and its own signing secret (shown once). An app can have several endpoints (for example one per system); the app's webhook URL is its default endpoint. Endpoints can also be managed with /v1/developer/apps/{id}/webhook-endpoints.
New apps subscribe to payment.capture.completed and payment.capture.declined by default. Subscribe to * to receive every event.
The event object
Every webhook body is the same event envelope, whether it is a live delivery, a retry, a simulated event or a dashboard “send test”. The type is a canonical registry id (e.g. payment.capture.completed), and the resource lives under data.object. Test events (livemode: false, from test keys, the simulator and “send test”) go only to test apps; live events only to live apps.
false for a test event (test keys, simulator, send test); true for real money
created
number
Unix timestamp (seconds) the event was created
data.object
object
The resource that triggered the event (it carries livemode too)
Signature verification
Each delivery carries a Watenga-Signature header. It contains a Unix timestamp and an HMAC-SHA256 signature, comma-separated:
plain
Watenga-Signature: t=1717406400,v1=5257a869e7ec3ead4f1b0f8b5b1f8b6f9f0c2e1d3a4b5c6d7e8f9a0b1c2d3e4f
# While a signing secret rotates, one v1 per secret:
Watenga-Signature: t=1717406400,v1=<signed with the new secret>,v1=<signed with the old secret>
The signed payload is the timestamp and the raw request body joined by a dot: `${t}.${rawBody}`. Recompute the HMAC-SHA256 with your endpoint's signing secret and accept the delivery when it equals any v1 in the header (constant-time comparison).
Verify before parsing
Verify the signature against the raw body before parsing JSON or touching your database. Reject events whose timestamp is more than 5 minutes (300s) old to defeat replay attacks, and always use a constant-time comparison such as crypto.timingSafeEqual.
Node.js
javascript
const crypto = require('crypto');
// Watenga-Signature: t=<unix-seconds>,v1=<hmac-sha256-hex>[,v1=<hmac>] (one v1 per secret while rotating)
function verify(rawBody, header, secret, toleranceSeconds = 300) {
let timestamp = NaN;
const signatures = [];
for (const part of String(header || '').split(',')) {
const [key, value] = part.split('=').map((s) => s && s.trim());
if (key === 't') timestamp = Number(value);
if (key === 'v1' && value) signatures.push(value);
}
if (!Number.isFinite(timestamp) || signatures.length === 0) return false;
// Reject replays: more than 5 minutes either way.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`, 'utf8').digest('hex'));
return signatures.some((sig) => {
const given = Buffer.from(sig);
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
if (!verify(req.rawBody, req.headers['watenga-signature'], process.env.WATENGA_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.rawBody); // { id, object: 'event', type, api_version, livemode, created, data: { object } }
PHP
php
$header = $_SERVER['HTTP_WATENGA_SIGNATURE'] ?? '';
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $segment) {
[$k, $v] = array_pad(explode('=', trim($segment), 2), 2, '');
if ($k === 't' && ctype_digit($v)) { $timestamp = (int) $v; }
if ($k === 'v1' && $v !== '') { $signatures[] = $v; } // one v1 per secret while rotating
}
// Reject replays: more than 5 minutes either way.
if ($timestamp === null || !$signatures || abs(time() - $timestamp) > 300) {
http_response_code(400);
exit;
}
$rawBody = file_get_contents('php://input');
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, getenv('WATENGA_WEBHOOK_SECRET'));
$valid = false;
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) { $valid = true; }
}
if (!$valid) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true); // $event['type'], $event['data']['object']
The SDKs verify for you (and accept two secrets while you rotate): Node.js, Python, PHP, Go and Dart.
Rotating a signing secret
Rotate an endpoint's secret in the developer console (two-step verification) and choose how long the old secret keeps signing: up to 7 days, 24 hours by default, or end it at once if it leaked. During the overlap every delivery carries a v1 signature for each secret, so your endpoint verifies with either while you deploy the new one; then remove the old secret from your configuration.
Testing on your machine
watenga listen --forward-to http://localhost:3000/webhooks forwards your test app's events to your machine, signed with a session secret it prints; watenga trigger payment.capture.completed sends one (CLI). No tunnel needed.
Retry policy
Answer with a 2xx status within 15 seconds; do the work after answering. Any other status, or no answer, is retried for about three days: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours, 20 hours and 24 hours (10 tries in all). The same event can arrive more than once: use its id to handle it once.
An endpoint that keeps failing is reported in the dashboard; one failing for three days is switched off (switch it back on in the console). Every delivery is in the app's delivery log, where you can send one again, or every failed one of an endpoint in a time window.
Events emitted today
These events are currently delivered by Watenga. The full registry — including events reserved for upcoming features — is available from GET /v1/meta/webhook-events and the catalogue below.
Event
Description
payment.capture.pending
A payment was started and waits for the buyer or the provider
payment.capture.completed
Card or wallet capture succeeded
payment.capture.declined
The provider declined or failed the payment
payment.capture.cancelled
The buyer cancelled the payment
payment.capture.expired
The payment session expired before the provider confirmed it; a later confirmation still credits it
payment.capture.refunded
Completed refunds now cover the whole payment
payment.capture.authorized
Payment authorized awaiting capture
split.settled
Net payment share credited to a subaccount
payment.authorization.created
Funds authorized on card
payment.authorization.voided
Authorization voided before capture
payment.refund.completed
Refund credited to customer
payment.refund.failed
Refund could not be processed
payment.refund.pending
Refund awaiting gateway or operator confirmation
payment.payout-item.succeeded
A payout to your bank or wallet was sent
payment.payout-item.failed
A payout failed; its amount is back in your available balance
payment.payout-item.returned
The receiving bank or wallet returned a payout
invoicing.invoice.sent
Invoice sent to customer
invoicing.invoice.paid
A payment paid the invoice in full
invoicing.invoice.partially-paid
A payment paid part of the invoice
invoicing.invoice.unpaid
Invoice became overdue
invoicing.invoice.cancelled
Invoice voided
billing.subscription.activated
Subscription is active
billing.subscription.cancelled
Subscription cancelled
billing.subscription.created
New subscription created
billing.subscription.expired
Subscription expired
billing.subscription.payment-failed
Recurring payment failed
billing.subscription.re-activated
Subscription re-activated
billing.subscription.suspended
Subscription suspended
billing.subscription.updated
Subscription plan or quantity updated
billing.plan.created
Billing plan created
customer.dispute.created
Chargeback or dispute opened
customer.dispute.updated
Dispute status changed
customer.dispute.resolved
Dispute won, lost, or accepted
customer.dispute.evidence-submitted
Dispute evidence package submitted
customer.payout.completed
A disbursement to a third party was paid
customer.payout.failed
A disbursement failed; its amount is back in your available balance
merchant.onboarding.submitted
Sub-merchant submitted KYC for review
merchant.onboarding.kyc-approved
Your KYC was approved
merchant.onboarding.kyc-rejected
Your KYC was rejected
vault.payment-token.created
Payment token vaulted
vault.payment-token.deleted
Payment token deleted
vault.payment-token.updated
Payment token updated
capital.offer_created
Merchant has a new Watenga Capital advance offer
capital.disbursed
Advance funds credited to merchant balance
capital.repayment
Holdback deducted from settlement
capital.repaid
Advance fully repaid
quotation.accepted
The customer accepted a quotation
quotation.declined
The customer declined a quotation
quotation.expired
A quotation expired
quotation.converted
A quotation became an invoice
quotation.deposit-paid
The deposit of a quotation was paid
booking.confirmed
A booking was confirmed (paid, or needing no payment)
booking.cancelled
A booking was cancelled
booking.rescheduled
A booking moved to another time
booking.completed
The customer attended
booking.no-show
The customer did not attend
donation.received
A gift or a recurring donation cycle was paid
trustpay.funds-held
The buyer paid; the money is held until release
trustpay.released
The held money was released to you
trustpay.dispute-opened
A dispute was opened on a TrustPay order
shop.order.paid
A shop order was paid
shop.order.payment-failed
A payment attempt for a shop order failed
shop.order.refunded
A shop order was refunded in part or in full
Full event catalogue
Generated from the canonical webhook event registry in @watenga/shared (mirrored to content/webhook-events.generated.md). “Live” events are delivered today; others are reserved for upcoming features.
PAYMENT_CAPTURE
Event
Description
Status
payment.capture.pending
A payment was started and waits for the buyer or the provider
Live
payment.capture.completed
Card or wallet capture succeeded
Live
payment.capture.declined
The provider declined or failed the payment
Live
payment.capture.cancelled
The buyer cancelled the payment
Live
payment.capture.expired
The payment session expired before the provider confirmed it; a later confirmation still credits it
Live
payment.capture.refunded
Completed refunds now cover the whole payment
Live
payment.capture.authorized
Payment authorized awaiting capture
Live
SPLIT
Event
Description
Status
split.settled
Net payment share credited to a subaccount
Live
PAYMENT_AUTHORIZATION
Event
Description
Status
payment.authorization.created
Funds authorized on card
Live
payment.authorization.voided
Authorization voided before capture
Live
PAYMENT_REFUND
Event
Description
Status
payment.refund.completed
Refund credited to customer
Live
payment.refund.failed
Refund could not be processed
Live
payment.refund.pending
Refund awaiting gateway or operator confirmation
Live
PAYOUT
Event
Description
Status
payment.payout-item.succeeded
A payout to your bank or wallet was sent
Live
payment.payout-item.failed
A payout failed; its amount is back in your available balance
Live
payment.payout-item.returned
The receiving bank or wallet returned a payout
Live
INVOICE
Event
Description
Status
invoicing.invoice.sent
Invoice sent to customer
Live
invoicing.invoice.paid
A payment paid the invoice in full
Live
invoicing.invoice.partially-paid
A payment paid part of the invoice
Live
invoicing.invoice.unpaid
Invoice became overdue
Live
invoicing.invoice.cancelled
Invoice voided
Live
SUBSCRIPTION
Event
Description
Status
billing.subscription.activated
Subscription is active
Live
billing.subscription.cancelled
Subscription cancelled
Live
billing.subscription.created
New subscription created
Live
billing.subscription.expired
Subscription expired
Live
billing.subscription.payment-failed
Recurring payment failed
Live
billing.subscription.re-activated
Subscription re-activated
Live
billing.subscription.suspended
Subscription suspended
Live
billing.subscription.updated
Subscription plan or quantity updated
Live
BILLING_PLAN
Event
Description
Status
billing.plan.created
Billing plan created
Live
DISPUTE
Event
Description
Status
customer.dispute.created
Chargeback or dispute opened
Live
customer.dispute.updated
Dispute status changed
Live
customer.dispute.resolved
Dispute won, lost, or accepted
Live
customer.dispute.evidence-submitted
Dispute evidence package submitted
Live
CUSTOMER_PAYOUT
Event
Description
Status
customer.payout.completed
A disbursement to a third party was paid
Live
customer.payout.failed
A disbursement failed; its amount is back in your available balance
Live
MERCHANT_ONBOARDING
Event
Description
Status
merchant.onboarding.submitted
Sub-merchant submitted KYC for review
Live
merchant.onboarding.kyc-approved
Your KYC was approved
Live
merchant.onboarding.kyc-rejected
Your KYC was rejected
Live
VAULT
Event
Description
Status
vault.payment-token.created
Payment token vaulted
Live
vault.payment-token.deleted
Payment token deleted
Live
vault.payment-token.updated
Payment token updated
Live
CAPITAL
Event
Description
Status
capital.offer_created
Merchant has a new Watenga Capital advance offer
Live
capital.disbursed
Advance funds credited to merchant balance
Live
capital.repayment
Holdback deducted from settlement
Live
capital.repaid
Advance fully repaid
Live
QUOTATION
Event
Description
Status
quotation.accepted
The customer accepted a quotation
Live
quotation.declined
The customer declined a quotation
Live
quotation.expired
A quotation expired
Live
quotation.converted
A quotation became an invoice
Live
quotation.deposit-paid
The deposit of a quotation was paid
Live
BOOKING
Event
Description
Status
booking.confirmed
A booking was confirmed (paid, or needing no payment)