Skip to main content

Webhooks Guide

Webhooks notify your backend when payment and payout states change. Your handler should verify the signature, process the event idempotently, and return a 2xx response quickly.
Prerequisites: You need a public callback URL and the same API secret used for MultiHub API request signing.

Events

EventDescription
payment.createdPayment was created
payment.processingPayment is processing
payment.completedPayment completed successfully
payment.failedPayment failed
payment.cancelledPayment was cancelled
payment.refundedPayment was refunded
payout.createdWithdrawal was created
payout.completedWithdrawal completed successfully
payout.failedWithdrawal failed

Payload

Webhook deliveries use an outer envelope. The payment object is under data.result.payment.
{
  "id": "pay_123:payment.completed",
  "created_at": "2026-04-02T08:23:04.379Z",
  "data": {
    "next": null,
    "result": {
      "payment": {
        "amount": { "value": 500000, "currency": "ARS" },
        "identifiers": {
          "c_id": "merchant-order-1",
          "h_id": "pay_123",
          "p_id": "provider-ref-123",
          "utr": "412345678901"
        },
        "status": {
          "status": "success",
          "final": true,
          "success": true,
          "error": null
        },
        "timestamps": {
          "created": "2026-04-02T08:22:21.453Z",
          "updated": "2026-04-02T08:22:22.795Z",
          "finished": "2026-04-02T08:22:21.790Z"
        },
        "destination": "in",
        "receiver": {},
        "operations": []
      }
    },
    "success": true,
    "request_id": "729aebbf-5a6b-4299-87fa-1cf05c1121a6",
    "processing_time": 0
  },
  "merchant_id": "19"
}
Internal fields prefixed with _ are not included in the delivered webhook body. Do not build integrations around _payment_id or other internal-only fields.
identifiers.utr is optional and appears only when a non-empty UTR/reference value is available.

Headers

HeaderDescription
Content-Typeapplication/json
X-Data-HashSHA-512 signature: SHA512(rawBody + API secret)
X-Webhook-IdDelivery/event identifier
X-Webhook-TimestampISO 8601 delivery timestamp
X-Webhook-NoncePer-delivery nonce
X-Webhook-Signature-V2Optional replay-resistant signature: SHA512(timestamp + rawBody + API secret)

Verify Signatures

Use the exact raw request body bytes. Re-serializing parsed JSON changes whitespace/key ordering and will break verification. Webhook deliveries created or rotated after the API secret rollout use the merchant API secret. Existing legacy webhook configurations may continue to verify with the previously issued signing secret until the merchant rotates the API secret.
import hashlib
import hmac
import json

API_SECRET = "your_api_secret"

def verify_webhook(raw_body: bytes, api_secret: str, received_hash: str) -> bool:
    expected = hashlib.sha512(raw_body + api_secret.encode()).hexdigest()
    return hmac.compare_digest(expected, received_hash or "")

# Flask-style handler
raw_body = request.get_data()
received_hash = request.headers.get("X-Data-Hash")

if not verify_webhook(raw_body, API_SECRET, received_hash):
    return {"error": "Invalid signature"}, 400

payload = json.loads(raw_body)
payment = payload["data"]["result"]["payment"]
status = payment["status"]["status"]

Retry Behavior

A webhook delivery succeeds on any HTTP 2xx response. Non-2xx responses, timeouts, network errors, URL validation failures, or circuit-breaker blocks are failures. Defaults:
SettingDefault
Timeout30 seconds
Max attempts3
Base retry delay1 second
Retry delay uses exponential backoff with jitter and a 24-hour cap. Delivery order is not guaranteed, so your handler must be idempotent.

Handling Statuses

Use payment.status.final to decide whether the payment reached a terminal state.
const payment = payload.data.result.payment;

if (payment.status.final && payment.status.success) {
  await markOrderPaid(payment.identifiers.c_id);
}

if (payment.status.final && payment.status.success === false) {
  await markOrderFailed(payment.identifiers.c_id, payment.status.error);
}

Per-Payment Webhook URLs

You can pass webhook_url in payment.in and payment.out requests:
{
  "method": "payment.in",
  "service_id": 14701,
  "params": {
    "payment": {
      "identifiers": { "c_id": "order-123" },
      "amount": { "value": 10000, "currency": "INR" },
      "payer": { "email": "customer@example.com" },
      "webhook_url": "https://merchant.example/webhooks/123hub"
    }
  }
}
The URL must be public HTTP(S). The gateway validates the URL when the payment is created and again before delivery.

Best Practices

  • Verify every signature before processing.
  • Return 2xx quickly and process heavy work asynchronously.
  • Use id plus payment.status.status as a deduplication key.
  • Store request_id, identifiers.c_id, and identifiers.h_id for support and reconciliation.
  • Treat webhook delivery as at-least-once.