Docs/Services/Webhooks

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

EventWhen
transaction.approvedA transaction was approved.
transaction.declinedA transaction was declined.
transaction.capturedAn authorization was captured.
transaction.voidedA transaction was voided.
transaction.refundedA transaction was refunded (full or partial).
transaction.reviewTransaction flagged for manual review.
transaction.chargebackChargeback filed.
transaction.chargeback_reversedChargeback reversed in your favor.
customer.created / customer.updated / customer.deletedVault mutations.
subscription.created / subscription.canceled / subscription.renewedRecurring lifecycle.
subscription.payment_succeeded / subscription.payment_failedEach renewal charge outcome.
invoice.sent / invoice.viewed / invoice.paid / invoice.overdueInvoice lifecycle.
simple_payment.paidHosted Simple Payment completed.
batch.completedAn uploaded transaction batch finished processing.
batch.settledA settlement batch was confirmed by the processor.
terminal.online / terminal.offlineTerminal 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")
  );
}
Verify before processing

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.