MPChatMPChat/Docs

Webhooks

Webhooks let your server receive real-time notifications when payment events occur — such as when a payment confirms, expires, or is flagged. This guide covers registering endpoints, verifying signatures, and handling retries.

Registering a webhook

Register a webhook endpoint to receive events for specific event types.

Register webhook
curl -X POST https://api.mpchat.com/v1/webhooks \
  -H "Authorization: Bearer {key_id}:{timestamp}:{signature}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://acme.com/webhooks/mpchat",
    "events": ["payment.confirmed", "payment.expired", "withdrawal.completed"]
  }'
Response
{
  "id": "wh_01HQ...",
  "url": "https://acme.com/webhooks/mpchat",
  "events": ["payment.confirmed", "payment.expired", "withdrawal.completed"],
  "is_active": true,
  "created_at": "2024-01-15T10:00:00Z"
}

Event types

payment.confirmed
event
A payment order was fully paid and confirmed on-chain. Fulfill the order.
payment.expired
event
An order expired without receiving payment. The address is released.
payment.underpaid
event
Payment received was below the required amount (outside tolerance). Manual review may be needed.
payment.frozen
event
A transaction was flagged by KYT compliance screening. Do not fulfill until cleared.
withdrawal.completed
event
A withdrawal was broadcast to the blockchain and confirmed.
withdrawal.failed
event
A withdrawal attempt failed. The funds are returned to your balance.

Payload structure

Every webhook request is an HTTP POST with a JSON body. Two custom headers are always present:

Example: payment.confirmed payload
// Headers
X-Merchant-Signature: t=1700000000,v1=a1b2c3d4e5f6...
X-Webhook-Event: payment.confirmed

// Body
{
  "event": "payment.confirmed",
  "timestamp": "2024-01-15T10:05:00Z",
  "data": {
    "order_id": "ord_01HQ...",
    "order_number": "ord_a1b2c3",
    "status": "PAID",
    "order_amount": "99.99",
    "received_amount": "99.99",
    "currency": "USDT",
    "network": "TRC20",
    "paid_at": "2024-01-15T10:04:58Z",
    "metadata": {
      "customer_id": "cust_123"
    }
  }
}

Verifying signatures

Always verify the X-Merchant-Signature header before processing any webhook. The signature prevents replay attacks and ensures the payload came from MP Merchant.

Signature format

The header value has the format:

text
X-Merchant-Signature: t={unix_timestamp},v1={hmac_hex}

The HMAC-SHA256 is computed over {timestamp}.{raw_body} using your webhook secret.

⚠️ Reject stale timestamps

Reject requests where the timestamp is more than 5 minutes in the past or future. This prevents replay attacks where an attacker captures a valid webhook and resends it later.

Verification code

JavaScript
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const [tPart, v1Part] = signature.split(',');
  const timestamp = tPart.replace('t=', '');
  const receivedHash = v1Part.replace('v1=', '');

  // Reject stale timestamps (5 minute tolerance)
  const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
  if (age > 300) throw new Error('Stale timestamp');

  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(receivedHash, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error('Invalid signature');
  }
}

Retry policy

If your endpoint returns a non-2xx status code or times out (10s timeout), the delivery is retried with exponential backoff:

AttemptDelayCumulative time
1 (immediate)0s
21 minute1m
35 minutes6m
430 minutes36m
52 hours~2h 36m
6 (final)24 hours~26h 36m

After 6 failed attempts, the delivery is marked FAILED and no further retries occur. You can view delivery history in the dashboard or via GET /v1/webhooks/{id}/deliveries.

💡 Idempotent handlers

Because of retries, your webhook handler may receive the same event multiple times. Use the data.order_id to de-duplicate — check if you've already processed this order before taking action.

Best practices

  • Respond quickly. Return HTTP 200 within 10 seconds. Defer heavy processing to a background job.
  • Always verify signatures. Never process a webhook without checking the HMAC.
  • Make handlers idempotent. The same event may be delivered more than once.
  • Reject stale timestamps. Add the 5-minute timestamp check shown above.
  • Use HTTPS endpoints only. HTTP endpoints are rejected.
  • Log raw payloads. Store the raw body before parsing to aid debugging.

Next steps