# Webhook Notifications

> HMAC-signed webhooks के माध्यम से real-time payment और payout status updates प्राप्त करें।

जब भी कोई payment status बदलता है, 2328.io सिस्टम आपके `url_callback` पर एक webhook भेजता है। सफल भुगतानों के बारे में सूचित होने का यह अनुशंसित तरीका है।

## Request format

- **Method:** `POST`
- **Content-Type:** `application/json`
- **Signature:** request body में `sign` field

## Payload

Webhook body `/v1/payment/info` response के format का पालन करता है और `tx_explorer_url` के साथ signature verification के लिए `sign` field जोड़ता है।

### सफल भुगतान

```json
{
  "uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
  "order_id": "ORDER-12345",
  "amount": "180.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
  "expires_at": "2026-05-09T16:56:58+03:00",
  "created_at": "2026-05-09T15:56:58+03:00",
  "payer_currency": "TON",
  "payer_amount": "0.95256917",
  "network": "TON",
  "address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
  "payment_status": "paid",
  "txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "tx_explorer_url": "https://tonviewer.com/transaction/41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "payment_amount": "0.95256917",
  "merchant_amount": "0.949711462490000000",
  "amount_usd": "2.41324380",
  "exchange_rate": "0.01340691",
  "sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}
```

### रद्द / असफल भुगतान

जब भुगतान terminal `paid` state में नहीं होता, तो `txid`, `payment_amount`, और `merchant_amount` `null` होते हैं:

```json
{
  "uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
  "order_id": "ORDER-12345",
  "amount": "2800.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
  "expires_at": "2026-05-09T06:19:04+03:00",
  "created_at": "2026-05-09T05:19:04+03:00",
  "payer_currency": "ETH",
  "payer_amount": "0.01620968",
  "network": "ETH-ERC20",
  "address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
  "payment_status": "cancel",
  "txid": null,
  "tx_explorer_url": null,
  "payment_amount": null,
  "merchant_amount": null,
  "amount_usd": "37.53934800",
  "exchange_rate": "0.01340691",
  "sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}
```

### Field reference

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | Payment UUID |
| `order_id` | string | आपका order ID |
| `amount` | decimal (8 dp) | `currency` में Fiat राशि |
| `currency` | string | वह fiat currency जिसका merchant ने अनुरोध किया |
| `url` | string | Hosted checkout URL |
| `expires_at` | string (ISO 8601) | Payment session कब expire होता है |
| `created_at` | string (ISO 8601) | Payment session कब बनाया गया |
| `payer_currency` | string | वह crypto जिसमें payer pay कर रहा है |
| `payer_amount` | decimal (8 dp) | अपेक्षित crypto राशि |
| `network` | string | Blockchain network |
| `address` | string | Deposit address |
| `payment_status` | string | इनमें से एक: `pending`, `check`, `paid`, `underpaid_check`, `underpaid`, `overpaid`, `cancel`, `aml_lock` (देखें [References](/docs/references)) |
| `txid` | string \| null | Blockchain tx hash, केवल confirmed payment के बाद उपस्थित |
| `tx_explorer_url` | string \| null | ब्लॉकचेन एक्सप्लोरर में ट्रांज़ैक्शन का URL। `txid` न होने या ट्रांसफ़र के आंतरिक P2P होने पर `null`। |
| `payment_amount` | decimal \| null | वास्तविक भुगतान राशि, केवल payment के बाद उपस्थित |
| `merchant_amount` | decimal (18 dp) \| null | Fees के बाद merchant को credit की गई राशि |
| `amount_usd` | decimal (8 dp) | Creation के समय USD में राशि |
| `exchange_rate` | decimal | उपयोग की गई Crypto / fiat विनिमय दर |
| `sign` | string (hex) | Payload का HMAC-SHA256 सिग्नेचर |

## सिग्नेचर verify करना

Webhook सिग्नेचर verify करने के लिए:

1. Payload से `sign` field निकालें
2. Object से `sign` field हटा दें
3. बाकी fields को JSON के रूप में encode करें
4. JSON को Base64 में encode करें
5. अपनी API_KEY का उपयोग करके Base64 string से HMAC-SHA256 compute करें
6. Constant-time comparison का उपयोग करके computed सिग्नेचर की तुलना `sign` value से करें

#### php

```php
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
    $receivedSign = $data['sign'] ?? '';
    unset($data['sign']);

    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    $calculated = hash_hmac('sha256', $base64, $apiKey);

    return hash_equals($calculated, $receivedSign);
}

$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);

if (!verifyWebhookSign($payload, $apiKey)) {
    http_response_code(401);
    exit;
}

switch ($payload['payment_status']) {
    case 'paid':
    case 'overpaid':
        // Credit the order — check idempotency by order_id first
        break;
    case 'underpaid_check':
    case 'underpaid':
    case 'cancel':
        break;
}

http_response_code(200);
```

#### js

```js
import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.json());

function verifyWebhookSign(payload, apiKey) {
  const { sign, ...rest } = payload;
  const json = JSON.stringify(rest);
  const base64 = Buffer.from(json).toString("base64");
  const calculated = crypto
    .createHmac("sha256", apiKey)
    .update(base64)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(calculated),
    Buffer.from(sign || ""),
  );
}

app.post("/webhook", (req, res) => {
  if (!verifyWebhookSign(req.body, process.env.API_KEY)) {
    return res.sendStatus(401);
  }

  const { order_id, payment_status, txid } = req.body;

  if (payment_status === "paid" || payment_status === "overpaid") {
    // Credit the order — check idempotency by order_id first
  }

  res.sendStatus(200);
});
```

#### python

```python
import json
import hmac
import hashlib
import base64
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
API_KEY = "YOUR_API_KEY"

def verify_webhook_sign(payload: dict, api_key: str) -> bool:
    received = payload.pop("sign", "")
    body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
    b64 = base64.b64encode(body.encode("utf-8")).decode()
    calculated = hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(calculated, received)

@app.post("/webhook")
async def webhook(request: Request):
    payload = await request.json()
    if not verify_webhook_sign(payload, API_KEY):
        raise HTTPException(401)

    if payload["payment_status"] in ("paid", "overpaid"):
        # Credit the order — check idempotency by order_id first
        pass

    return {"ok": True}
```

#### go

```go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
)

func verifyWebhookSign(body []byte, apiKey string) (map[string]any, bool) {
    var payload map[string]any
    if err := json.Unmarshal(body, &payload); err != nil {
        return nil, false
    }
    received, _ := payload["sign"].(string)
    delete(payload, "sign")

    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    enc.Encode(payload)
    reencoded := bytes.TrimRight(buf.Bytes(), "\n")

    b64 := base64.StdEncoding.EncodeToString(reencoded)
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    calculated := hex.EncodeToString(h.Sum(nil))

    return payload, hmac.Equal([]byte(calculated), []byte(received))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    payload, ok := verifyWebhookSign(body, apiKey)
    if !ok {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    status, _ := payload["payment_status"].(string)
    if status == "paid" || status == "overpaid" {
        // Credit the order — check idempotency first
    }
    w.WriteHeader(http.StatusOK)
}
```

#### ruby

```ruby
require "json"
require "openssl"
require "base64"
require "sinatra"

API_KEY = "YOUR_API_KEY"

def verify_webhook_sign(payload, api_key)
  received = payload.delete("sign") || ""
  body = payload.to_json
  b64 = Base64.strict_encode64(body)
  calculated = OpenSSL::HMAC.hexdigest("SHA256", api_key, b64)
  OpenSSL.fixed_length_secure_compare(calculated, received)
end

post "/webhook" do
  payload = JSON.parse(request.body.read)
  halt 401 unless verify_webhook_sign(payload, API_KEY)

  if %w[paid overpaid].include?(payload["payment_status"])
    # Credit the order — check idempotency by order_id first
  end

  status 200
end
```

> **DANGER:** **किसी भी user को फंड credit करने से पहले हमेशा सिग्नेचर verify करें।** एक unsigned या गलत-तरीके से signed webhook spoofed request हो सकता है।

## Payout webhooks

जब किसी payout का `status` बदलता है, तो सिस्टम payout बनाते समय pass किए गए `url_callback` URL पर एक `POST` webhook भेजता है। यदि `url_callback` प्रदान नहीं किया गया था, तो उस payout के लिए कोई webhook नहीं भेजा जाता।

> **WARNING:** Payout webhooks को आपकी **Payout API key** से verify किया जाना चाहिए — सामान्य API key से नहीं। Signing algorithm payment webhooks के समान ही है (`sign` हटाएँ, JSON-encode, base64, HMAC-SHA256), केवल key अलग है।

### Payload

```json
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "tx_explorer_url": "https://tronscan.org/#/transaction/9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}
```

### Field reference

| Field | Type | Description |
|-------|------|-------------|
| `uuid` | string | Payout UUID |
| `order_id` | string | आपका idempotency / reference ID, यदि आपने प्रदान किया हो |
| `status` | string | `pending`, `completed`, `failed`, `cancelled` (देखें [References](/docs/references)) |
| `currency` | string | निकासी currency |
| `network` | string | Blockchain network |
| `amount` | decimal | निकासी राशि (`currency` में) |
| `merchant_amount` | decimal | Merchant बैलेंस से charge की गई राशि |
| `network_amount` | decimal | वास्तव में on-chain भेजी गई राशि |
| `amount_usd` | decimal | Payout के समय USD value |
| `to_address` | string | प्राप्तकर्ता का blockchain address |
| `memo` | string \| null | यदि उपयोग किया गया हो तो Memo / destination tag |
| `txid` | string \| null | Blockchain transaction hash, `completed` पर set होता है |
| `tx_explorer_url` | string \| null | ब्लॉकचेन एक्सप्लोरर में ट्रांज़ैक्शन का URL। `txid` न होने या ट्रांसफ़र के आंतरिक P2P होने पर `null`। |
| `block_number` | integer \| null | On-chain transaction की Block height |
| `error_type` | string \| null | जब `status = failed` का कारण (जैसे `aml_risk`, देखें [References](/docs/references)) |
| `created_at` | string (ISO 8601) | Payout कब बनाया गया |
| `updated_at` | string (ISO 8601) | Status अंतिम बार कब बदला गया |
| `from_currency` | string | Auto-conversion का उपयोग होने पर वह source बैलेंस जिससे payout debit हुआ था (जैसे `BTC` payout के लिए `USDT`) |
| `debited_amount` | decimal | `from_currency` बैलेंस से debit की गई राशि |
| `debited_currency` | string | Debit की currency |
| `sign` | string (hex) | Payload का HMAC-SHA256 सिग्नेचर, **Payout API key** से signed |

## Best practices

- **Idempotency** — हमेशा जाँच करें कि भुगतान पहले से process हो चुका है (या तो `order_id` या `uuid` से)। Webhooks कई बार आ सकते हैं।
- **तेज़ response** — जितनी जल्दी हो सके HTTP 200 लौटाएँ। भारी काम को background queue में स्थानांतरित करें।
- **Retries** — यदि सिस्टम को HTTP 200 नहीं मिलता, तो webhook 2 मिनट बाद फिर से भेजा जाता है। अधिकतम 5 retry प्रयास।
- **Async processing** — Response को block होने से बचाने के लिए webhook events को asynchronously handle करें।
- **Security** — Payload पर भरोसा करने से पहले हमेशा `sign` सिग्नेचर verify करें।

> **WARNING:** Webhooks order से बाहर आ सकते हैं। यह न मानें कि आपको प्राप्त पहला webhook ही final state है — यदि आपको निश्चितता चाहिए तो हमेशा `/v1/payment/info` (या `/v1/payout/status/{uuid}`) के माध्यम से दोबारा fetch करें।

## डिलिवरी और प्रोसेसिंग अनुबंध

अपने वेबहुक एंडपॉइंट के अंदर निम्नलिखित आदेश का उपयोग करें:

1. रिक्वेस्ट बॉडी को पढ़ें बिना रहस्य या पूर्ण सिग्नेचर को लॉग किए।
2. पहचानें कि यह भुगतान/स्थिर-वॉलेट इवेंट है या भुगतान इवेंट, ताकि आप सही API कुंजी का चयन कर सकें।
3. `sign` को हटाएं, दर्ज दस्तावेज़ीकृत JSON बाइट्स को पुन: उत्पन्न करें, HMAC-SHA256 की गणना करें, और लगातार समय में तुलना करें।
4. आवश्यक पहचानकर्ताओं, दशमलव स्ट्रिंग्स, और स्थिति मानों को सत्यापित करें।
5. एक इनबॉक्स/इडेम्पोटेंसी रिकॉर्ड परमाणुवत रूप से डालें। अगर यह पहले से मौजूद है, तो साइड इफेक्ट्स को दोहराए बिना HTTP 200 लौटाएं।
6. ऑर्डर/लेज़र म्यूटेशन को कमिट करें और गैर-महत्वपूर्ण ईमेल, एनालिटिक्स, या नोटिफिकेशन को कतार में डालें।
7. HTTP 200 जल्दी लौटाएँ।

आईडेम्पोटेंसी ट्रांजैक्शन होल्ड करते समय धीमी थर्ड-पार्टी सेवाओं को कॉल न करें। कमिट करने के बाद लेकिन लौटाने से पहले टाइमआउट होने पर रीट्राय हो सकता है; डुप्लिकेट को कमिटेड इनबॉक्स की कुंजी का पालन करना चाहिए और यह नो-ऑप हो जाना चाहिए।

### सिफारिश किए गए आईडेम्पोटेंसी कुंजी

| ईवेंट | प्राथमिक पहचान | नोट्स |
|-------|------------------|-------|
| पेमेंट सेशन | `uuid` + स्थिति/संस्करण साक्ष्य | एक ही इनवॉइस कई वैध स्थिति परिवर्तन उत्पन्न कर सकता है। |
| आंशिक-भुगतान टॉप-अप | चालान `uuid` + `txid` | एक से अधिक ट्रांसफर उसी अधभर भुगतान चालान से संबंधित हो सकते हैं। |
| स्टैटिक-वॉलेट जमा | नेटवर्क + `txid` | `order_id` उस वॉलेट में हर जमा द्वारा पुन: उपयोग किया जाता है। |
| भुगतान | भुगतान `uuid` + स्थिति | वेबहूक रीट्राई लॉजिक से कभी दूसरा भुगतान न बनाएं। |

यदि आपके स्कीमा में कोई ईवेंट आईडी नहीं है, तो अतिरिक्त ऑडिट साक्ष्य के रूप में सत्यापित पेलोड हैश संग्रहीत करें, लेकिन उपरोक्त बिजनेस पहचान को टाइमस्टैम्प से प्रतिस्थापित न करें।

## ऑर्डरिंग और समेकन

डिलीवरी कम से कम एक बार होती है और स्थिति संदेश प्रतिस्पर्धा कर सकते हैं। "अंतिम अनुरोध जीतता है" की बजाय मोनोटोनिक व्यावसायिक नियम लागू करें:

- कभी भी एक पूरा किया गया ऑर्डर `check` में वापस न ले जाएँ क्योंकि कोई पुरानी घटना देर से आई है;
- `underpaid_check` को अतिरिक्त txids प्राप्त करने की अनुमति दें बिना पहले के क्रेडिट को दोहराए;
- `paid` और `overpaid` को सफल निपटान की स्थिति के रूप में मानें, जबकि उनके अलग-अलग राशि को बनाए रखें;
- `underpaid` को अंतिम आंशिक-भुगतान परिणाम के रूप में रखें जब तक कि अधिकारिक API बाद में किसी अन्य स्थिति की रिपोर्ट न करे;
- `aml_lock` को समीक्षा के लिए मार्गित करें और किसी सामान्य पुन: प्रयास कार्यकर्ता को इसे पूरा करने न दें;
- जब संक्रमण असंभव हो, संदर्भ गायब हो, या वित्तीय रूप से अस्पष्ट हो, तो भुगतान/पेआउट जानकारी पूछें।

यह सुनिश्चित करने के लिए कि वेबहुक वितरण स्वस्थ दिखाई दे रहा है, अनुसूचित मिलान चलाएँ। अपने स्थानीय टर्मिनल स्थिति और क्रेडिट किए गए राशि की तुलना `/v1/payment/info`, `/v1/static-wallet/transactions`, या `/v1/payout/status/{uuid}` के साथ करें और अंतर पर अलर्ट करें, न कि लेज़र इतिहास को चुपचाप अधिलेखित करें।

## वेबहुक एंडपॉइंट सुरक्षा

- HTTPS की आवश्यकता रखें और कॉलबैक को सार्वजनिक रूप से पहुंच योग्य बनाए रखें; पेमेंट निर्माण के दौरान निजी/लूपबैक कॉलबैक लक्ष्य अस्वीकार किए जाते हैं।
- एक छोटे अनुरोध-बॉडी सीमा और JSON सामग्री प्रकार लागू करें।
- महंगे कार्य से पहले रेट-लिमिट करें, लेकिन वैध बर्स्ट और पुनः प्रयास के लिए पर्याप्त हेडरूम छोड़ दें।
- केवल स्रोत IP द्वारा किसी वेबहुक को कभी अधिकृत न करें। नेटवर्क अलाउलिस्ट गहराई में सुरक्षा हैं; HMAC सत्यापन अनिवार्य है।
- `sign`, API कुंजी, पते, जब नीति द्वारा आवश्यक हो तो, और व्यक्तिगत मेटाडेटा को एप्लिकेशन लॉग से हटा दें।
- नियंत्रित घुमाव विंडो के दौरान वर्तमान और स्पष्ट रूप से निर्धारित प्रतिस्थापन कुंजियों दोनों को उपलब्ध रखें; कभी भी अनुमान न लगाएँ कि किसी घटना को किस कुंजी ने साइन किया।
- अमान्य हस्ताक्षरों पर एक सामान्य त्रुटि संदेश लौटाएँ ताकि एंडपॉइंट कुंजी या खाता ओरेकल न बन सके।