Webhook Event Types

The payment gateway sends webhook events to notify your application about payment lifecycle changes.

Webhook Headers

All webhook requests include these headers:

X-PaymentService-Event: payment.completed
X-PaymentService-Timestamp: 1706356245
X-PaymentService-Signature: a1b2c3d4e5f6...
Content-Type: application/json
HeaderDescription
X-PaymentService-EventThe event type that triggered this webhook
X-PaymentService-TimestampUnix timestamp when the webhook was sent
X-PaymentService-SignatureHMAC-SHA256 signature for verification

Payload Structure

All webhook payloads contain the full payment object:

{
  "payment": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "userId": "...",
    "status": "RECEIVED",
    "reference": "ORDER-123",
    "referenceLabel": "Order #123",
    "currencyCode": "USD",
    "settlementCurrencyCode": "USDT",
    "amount": {
      "value": 100.00,
      "amount": 10000,
      "formatted": "$100.00"
    },
    "receivedAmount": {
      "value": 100.00,
      "amount": 10000,
      "formatted": "$100.00"
    },
    "settledAmount": {
      "value": 100.00,
      "amount": 10000,
      "formatted": "$100.00"
    },
    "confirmedAt": "2026-01-15T10:30:00Z",
    "foundAt": "2026-01-15T10:15:00Z",
    "date": "2026-01-15T10:00:00Z"
  }
}

Available Events

payment.completed

Triggered when the full payment amount is received and confirmed on‑chain. This is the terminal success state.

Status: RECEIVED

This is the primary event to listen for — it indicates the payment is complete and you should fulfill the order. Key your fulfillment off the event type (payment.completed), not the status string.

payment.received

Triggered when funds are received and confirmed on the blockchain.

Status: RECEIVED

This fires when the blockchain confirms the transaction. Use this if you want to act before the full confirmation cycle.

payment.underpaid

Triggered when the received amount is less than the requested amount.

Status: UNDERPAID

{
  "payment": {
    "id": "...",
    "status": "UNDERPAID",
    "amount": {
      "value": 100.00,
      "amount": 10000,
      "formatted": "$100.00"
    },
    "receivedAmount": {
      "value": 95.00,
      "amount": 9500,
      "formatted": "$95.00"
    },
    "remainingAmount": {
      "value": 5.00,
      "amount": 500,
      "formatted": "$5.00"
    }
  }
}

payment.refund

Triggered when a refund is completed for a payment.

Status: REFUND

{
  "payment": {
    "id": "...",
    "status": "REFUND",
    "refund": true,
    "refundedAmount": {
      "value": 100.00,
      "amount": 10000,
      "formatted": "$100.00"
    }
  }
}

Event Summary

EventDescriptionPayment Status
payment.completedPayment fully receivedRECEIVED
payment.receivedFunds received on blockchainRECEIVED
payment.underpaidInsufficient amount receivedUNDERPAID
payment.refundRefund completedREFUND

Handling Events

JavaScript Example

app.post('/webhooks/payments', (req, res) => {
  const eventType = req.headers['x-paymentservice-event'];
  const payment = req.body.payment;

  switch (eventType) {
    case 'payment.completed':
      // Payment confirmed — fulfill the order
      await updateOrderStatus(payment.reference, 'paid');
      break;

    case 'payment.received':
      // Funds received — optional early notification
      await notifyPaymentReceived(payment.reference);
      break;

    case 'payment.underpaid':
      // Underpayment — notify customer
      await handleUnderpayment(payment);
      break;

    case 'payment.refund':
      // Refund completed — update records
      await handleRefund(payment);
      break;
  }

  res.status(200).send('OK');
});

Best Practices

  1. Always verify the webhook signature before processing events
  2. Process events idempotently — the same event may be delivered multiple times
  3. Respond with 200 quickly — process async if needed to avoid timeouts
  4. Log all received events for debugging and audit trails
  5. Handle unknown event types gracefully — don't crash on unrecognized events

Next Steps