Webhooks

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.

json
{
  "id": "evt_550e8400-e29b-41d4-a716-446655440000",
  "object": "event",
  "type": "payment.capture.completed",
  "api_version": "2026-10-29",
  "livemode": false,
  "created": 1717406400,
  "data": {
    "object": {
      "id": "txn_01HQXYZ",
      "object": "transaction",
      "amount": "25.0000",
      "currency": "USD",
      "status": "completed",
      "merchant_transaction_id": "ORD-1001",
      "metadata": { "order_id": "ORD-1001" },
      "livemode": false
    }
  }
}
FieldTypeDescription
idstringUnique event id, prefixed evt_
objectstringAlways “event”
typestringCanonical event type from the registry
api_versionstringThe API version the event was rendered with
livemodebooleanfalse for a test event (test keys, simulator, send test); true for real money
creatednumberUnix timestamp (seconds) the event was created
data.objectobjectThe 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.

EventDescription
payment.capture.pendingA payment was started and waits for the buyer or the provider
payment.capture.completedCard or wallet capture succeeded
payment.capture.declinedThe provider declined or failed the payment
payment.capture.cancelledThe buyer cancelled the payment
payment.capture.expiredThe payment session expired before the provider confirmed it; a later confirmation still credits it
payment.capture.refundedCompleted refunds now cover the whole payment
payment.capture.authorizedPayment authorized awaiting capture
split.settledNet payment share credited to a subaccount
payment.authorization.createdFunds authorized on card
payment.authorization.voidedAuthorization voided before capture
payment.refund.completedRefund credited to customer
payment.refund.failedRefund could not be processed
payment.refund.pendingRefund awaiting gateway or operator confirmation
payment.payout-item.succeededA payout to your bank or wallet was sent
payment.payout-item.failedA payout failed; its amount is back in your available balance
payment.payout-item.returnedThe receiving bank or wallet returned a payout
invoicing.invoice.sentInvoice sent to customer
invoicing.invoice.paidA payment paid the invoice in full
invoicing.invoice.partially-paidA payment paid part of the invoice
invoicing.invoice.unpaidInvoice became overdue
invoicing.invoice.cancelledInvoice voided
billing.subscription.activatedSubscription is active
billing.subscription.cancelledSubscription cancelled
billing.subscription.createdNew subscription created
billing.subscription.expiredSubscription expired
billing.subscription.payment-failedRecurring payment failed
billing.subscription.re-activatedSubscription re-activated
billing.subscription.suspendedSubscription suspended
billing.subscription.updatedSubscription plan or quantity updated
billing.plan.createdBilling plan created
customer.dispute.createdChargeback or dispute opened
customer.dispute.updatedDispute status changed
customer.dispute.resolvedDispute won, lost, or accepted
customer.dispute.evidence-submittedDispute evidence package submitted
customer.payout.completedA disbursement to a third party was paid
customer.payout.failedA disbursement failed; its amount is back in your available balance
merchant.onboarding.submittedSub-merchant submitted KYC for review
merchant.onboarding.kyc-approvedYour KYC was approved
merchant.onboarding.kyc-rejectedYour KYC was rejected
vault.payment-token.createdPayment token vaulted
vault.payment-token.deletedPayment token deleted
vault.payment-token.updatedPayment token updated
capital.offer_createdMerchant has a new Watenga Capital advance offer
capital.disbursedAdvance funds credited to merchant balance
capital.repaymentHoldback deducted from settlement
capital.repaidAdvance fully repaid
quotation.acceptedThe customer accepted a quotation
quotation.declinedThe customer declined a quotation
quotation.expiredA quotation expired
quotation.convertedA quotation became an invoice
quotation.deposit-paidThe deposit of a quotation was paid
booking.confirmedA booking was confirmed (paid, or needing no payment)
booking.cancelledA booking was cancelled
booking.rescheduledA booking moved to another time
booking.completedThe customer attended
booking.no-showThe customer did not attend
donation.receivedA gift or a recurring donation cycle was paid
trustpay.funds-heldThe buyer paid; the money is held until release
trustpay.releasedThe held money was released to you
trustpay.dispute-openedA dispute was opened on a TrustPay order
shop.order.paidA shop order was paid
shop.order.payment-failedA payment attempt for a shop order failed
shop.order.refundedA 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

EventDescriptionStatus
payment.capture.pendingA payment was started and waits for the buyer or the providerLive
payment.capture.completedCard or wallet capture succeededLive
payment.capture.declinedThe provider declined or failed the paymentLive
payment.capture.cancelledThe buyer cancelled the paymentLive
payment.capture.expiredThe payment session expired before the provider confirmed it; a later confirmation still credits itLive
payment.capture.refundedCompleted refunds now cover the whole paymentLive
payment.capture.authorizedPayment authorized awaiting captureLive

SPLIT

EventDescriptionStatus
split.settledNet payment share credited to a subaccountLive

PAYMENT_AUTHORIZATION

EventDescriptionStatus
payment.authorization.createdFunds authorized on cardLive
payment.authorization.voidedAuthorization voided before captureLive

PAYMENT_REFUND

EventDescriptionStatus
payment.refund.completedRefund credited to customerLive
payment.refund.failedRefund could not be processedLive
payment.refund.pendingRefund awaiting gateway or operator confirmationLive

PAYOUT

EventDescriptionStatus
payment.payout-item.succeededA payout to your bank or wallet was sentLive
payment.payout-item.failedA payout failed; its amount is back in your available balanceLive
payment.payout-item.returnedThe receiving bank or wallet returned a payoutLive

INVOICE

EventDescriptionStatus
invoicing.invoice.sentInvoice sent to customerLive
invoicing.invoice.paidA payment paid the invoice in fullLive
invoicing.invoice.partially-paidA payment paid part of the invoiceLive
invoicing.invoice.unpaidInvoice became overdueLive
invoicing.invoice.cancelledInvoice voidedLive

SUBSCRIPTION

EventDescriptionStatus
billing.subscription.activatedSubscription is activeLive
billing.subscription.cancelledSubscription cancelledLive
billing.subscription.createdNew subscription createdLive
billing.subscription.expiredSubscription expiredLive
billing.subscription.payment-failedRecurring payment failedLive
billing.subscription.re-activatedSubscription re-activatedLive
billing.subscription.suspendedSubscription suspendedLive
billing.subscription.updatedSubscription plan or quantity updatedLive

BILLING_PLAN

EventDescriptionStatus
billing.plan.createdBilling plan createdLive

DISPUTE

EventDescriptionStatus
customer.dispute.createdChargeback or dispute openedLive
customer.dispute.updatedDispute status changedLive
customer.dispute.resolvedDispute won, lost, or acceptedLive
customer.dispute.evidence-submittedDispute evidence package submittedLive

CUSTOMER_PAYOUT

EventDescriptionStatus
customer.payout.completedA disbursement to a third party was paidLive
customer.payout.failedA disbursement failed; its amount is back in your available balanceLive

MERCHANT_ONBOARDING

EventDescriptionStatus
merchant.onboarding.submittedSub-merchant submitted KYC for reviewLive
merchant.onboarding.kyc-approvedYour KYC was approvedLive
merchant.onboarding.kyc-rejectedYour KYC was rejectedLive

VAULT

EventDescriptionStatus
vault.payment-token.createdPayment token vaultedLive
vault.payment-token.deletedPayment token deletedLive
vault.payment-token.updatedPayment token updatedLive

CAPITAL

EventDescriptionStatus
capital.offer_createdMerchant has a new Watenga Capital advance offerLive
capital.disbursedAdvance funds credited to merchant balanceLive
capital.repaymentHoldback deducted from settlementLive
capital.repaidAdvance fully repaidLive

QUOTATION

EventDescriptionStatus
quotation.acceptedThe customer accepted a quotationLive
quotation.declinedThe customer declined a quotationLive
quotation.expiredA quotation expiredLive
quotation.convertedA quotation became an invoiceLive
quotation.deposit-paidThe deposit of a quotation was paidLive

BOOKING

EventDescriptionStatus
booking.confirmedA booking was confirmed (paid, or needing no payment)Live
booking.cancelledA booking was cancelledLive
booking.rescheduledA booking moved to another timeLive
booking.completedThe customer attendedLive
booking.no-showThe customer did not attendLive

DONATION

EventDescriptionStatus
donation.receivedA gift or a recurring donation cycle was paidLive

TRUSTPAY

EventDescriptionStatus
trustpay.funds-heldThe buyer paid; the money is held until releaseLive
trustpay.releasedThe held money was released to youLive
trustpay.dispute-openedA dispute was opened on a TrustPay orderLive

SHOP

EventDescriptionStatus
shop.order.paidA shop order was paidLive
shop.order.payment-failedA payment attempt for a shop order failedLive
shop.order.refundedA shop order was refunded in part or in fullLive