Terminal payment webhooks

API request

VERIFY webhook-signature

Verify X-Mainmoney-Signature on incoming webhook payloads.

# Verify HMAC-SHA256 signature (pseudo)
# signature = HMAC_SHA256(webhook_secret, raw_request_body)

Terminal payment webhooks

Configure webhookUrl on your developer app. MainMoney POSTs JSON when a terminal payment reaches a terminal state.

Events

EventWhen
terminal_payment.completedCashier completed payment on the terminal
terminal_payment.failedPayment failed on the terminal
terminal_payment.cancelledYou cancelled a PENDING request
terminal_payment.expiredTTL elapsed without completion

Headers

HeaderDescription
X-Mainmoney-EventEvent name (same as event in body)
X-Mainmoney-SignatureHex HMAC-SHA256 of the raw JSON body using your app whsec_… secret
Content-Typeapplication/json

Verify signature

  1. Read the raw request body bytes (do not re-serialize).
  2. HMAC-SHA256(signingSecret, rawBody) → hex digest.
  3. Compare to X-Mainmoney-Signature with a constant-time equals.

Reject the request if the signature does not match.

Payload

{
  "event": "terminal_payment.completed",
  "id": "...",
  "created": "2026-07-27T10:12:00.000Z",
  "data": {
    "id": "clx...",
    "merchantReference": "crm-order-1001",
    "metadata": { "table": "12" },
    "terminalId": "POS-ABC123-001",
    "businessId": "...",
    "amount": 1.03,
    "currencyCode": "USD",
    "status": "COMPLETED",
    "paymentMethod": "PHONE",
    "paymentRequestId": "...",
    "environment": "SANDBOX",
    "expiresAt": "2026-07-27T10:16:00.000Z",
    "completedAt": "2026-07-27T10:12:01.000Z",
    "cancelledAt": null,
    "createdAt": "2026-07-27T10:11:00.000Z"
  }
}

paymentMethod is set when the cashier finishes: PALM | PHONE | QR | CASH.

Local testing

Use the terminal-payments-demo with ngrok so MainMoney can reach POST /webhooks. See that repo’s README.

See also