Webhooks
Subscribe to account events and receive signed POST callbacks. Essential for keeping your system in sync with async events like settlement, chargebacks, and recurring renewals.
A webhook is a POST request moat sends to a URL you control when a specific event happens on your account. Use webhooks to react to things you did not directly initiate: a recurring renewal charge, a settlement landing, a chargeback being filed, a customer opening an invoice.
Configure an endpoint
Endpoints are managed in the Control Panel under Settings → Webhooks. For each endpoint you configure:
- URL — must be HTTPS and publicly reachable.
- Events — which event types to subscribe to. You can subscribe to specific events or use
*to receive everything. - Signing secret — auto-generated. Used to verify authenticity of inbound webhooks.
Event catalog
| Event | When |
|---|---|
transaction.approved | A transaction was approved. |
transaction.declined | A transaction was declined. |
transaction.captured | An authorization was captured. |
transaction.voided | A transaction was voided. |
transaction.refunded | A transaction was refunded (full or partial). |
transaction.review | Transaction flagged for manual review. |
transaction.chargeback | Chargeback filed. |
transaction.chargeback_reversed | Chargeback reversed in your favor. |
customer.created / customer.updated / customer.deleted | Vault mutations. |
subscription.created / subscription.canceled / subscription.renewed | Recurring lifecycle. |
subscription.payment_succeeded / subscription.payment_failed | Each renewal charge outcome. |
invoice.sent / invoice.viewed / invoice.paid / invoice.overdue | Invoice lifecycle. |
simple_payment.paid | Hosted Simple Payment completed. |
batch.completed | An uploaded transaction batch finished processing. |
batch.settled | A settlement batch was confirmed by the processor. |
terminal.online / terminal.offline | Terminal connectivity state changes. |
Payload shape
{
"id": "evt_01H9XK...",
"event": "transaction.approved",
"created_at": "2026-04-23T10:00:00Z",
"data": {
"id": "txn_01H9XK...",
"amount": 2500,
"status": "approved",
"customer_id": "cust_01H9XK..."
// Full resource as it would come back from GET /api/transaction/{id}
}
}
Signing and verification
Every webhook includes two HTTP headers:
X-Signature— HMAC-SHA256 of the request body, hex encoded.X-Signature-Timestamp— Unix epoch seconds when the webhook was dispatched.
Verifying (Node.js)
const crypto = require("crypto");
function verifyWebhook(rawBody, timestamp, signature, secret) {
// Reject if timestamp is older than 5 minutes to prevent replay
if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) return false;
const payload = timestamp + "." + rawBody;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
An unverified webhook can be forged by anyone who knows your URL. Always verify the signature before acting on a webhook. A failed verification should return 400 and log — never process the payload.
Retries
If your endpoint does not respond with a 2xx within 10 seconds, or returns a 5xx, moat retries with exponential backoff: at 30 seconds, then 2 minutes, 10, 30, 60, 120, 240 minutes. After 8 failed attempts, the event is abandoned and marked as failed in the webhook log.
Idempotency on your side
Because retries are possible, treat the event id as an idempotency key. Keep a set of recently processed event IDs (24-hour TTL is plenty) and short-circuit any repeats.
Testing locally
For local development, use a tunneling service (ngrok, Cloudflare Tunnel, etc.) to expose a local port to a public URL you can configure as the webhook endpoint. The Control Panel's webhook log lets you replay any past event to your endpoint on demand.