> ## Documentation Index
> Fetch the complete documentation index at: https://docs.txncheck.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks Guide

> Receive real-time notifications about verification results

# Webhooks Guide

Webhooks allow you to receive real-time HTTP notifications when verification requests complete. Instead of polling for results, your server receives automatic updates as soon as processing finishes.

## How Webhooks Work

<Steps>
  <Step title="Submit Request">
    You submit a verification request (e.g., UPI by Mobile)
  </Step>

  <Step title="Request Queued">
    API returns `202 Accepted` with a request ID
  </Step>

  <Step title="Processing">
    TxnCheck processes the verification request
  </Step>

  <Step title="Webhook Delivered">
    Results are sent to your configured webhook URL
  </Step>

  <Step title="Confirmation">
    Your server returns `2xx` to acknowledge receipt
  </Step>
</Steps>

## Setting Up Webhooks

Webhook URLs are configured at the merchant account level. Contact your account manager or configure webhooks in the [Merchant Dashboard](https://dashboard.txncheck.in) under **Settings → Webhooks**.

### Requirements

* **HTTPS URL** - Webhook endpoints must use HTTPS
* **Public accessibility** - Your endpoint must be reachable from the internet
* **Quick response** - Return `2xx` within 30 seconds

## Webhook Events

| Event               | Description                                          |
| ------------------- | ---------------------------------------------------- |
| `request.completed` | Verification completed successfully                  |
| `request.failed`    | Verification failed                                  |
| `request.partial`   | Verification partially completed (some steps failed) |

## Webhook Payload

All webhook payloads follow this structure:

```json theme={null}
{
  "event": "request.completed",
  "timestamp": "2025-01-11T10:30:00.000Z",
  "data": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "method": "upi-by-mobile",
    "status": "COMPLETED",
    "result": {
      "name": "JOHN DOE",
      "upi": [
        "johndoe@upi",
        "9876543210@paytm"
      ],
      "status": "1"
    },
    "completedAt": "2025-01-11T10:30:00.000Z"
  }
}
```

### Payload Fields

| Field               | Type   | Description                                                           |
| ------------------- | ------ | --------------------------------------------------------------------- |
| `event`             | string | Event type (`request.completed`, `request.failed`, `request.partial`) |
| `timestamp`         | string | ISO 8601 timestamp when webhook was generated                         |
| `data.requestId`    | string | Unique request identifier                                             |
| `data.method`       | string | Verification method used                                              |
| `data.status`       | string | Final request status                                                  |
| `data.result`       | object | Verification result data (if successful)                              |
| `data.error`        | object | Error details (if failed)                                             |
| `data.stepStatuses` | object | Individual step statuses (for full-check)                             |
| `data.completedAt`  | string | When the request completed                                            |

### Full Check Step Statuses

For `full-check` requests, the `stepStatuses` field shows the outcome of each verification step:

```json theme={null}
{
  "event": "request.completed",
  "timestamp": "2025-01-11T10:30:00.000Z",
  "data": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "method": "full-check",
    "status": "COMPLETED",
    "stepStatuses": {
      "upiByMobile": "ok",
      "kycByMobile": "ok",
      "vpaChargebackCheck": "ok"
    },
    "result": {
      "upiByMobile": {
        "name": "JOHN DOE",
        "vpas": ["johndoe@upi", "9876543210@paytm"],
        "status": "1"
      },
      "kycByMobile": {
        "fullName": "JOHN DOE",
        "pan": "ABCDE1234F",
        "maskedAadhaar": "XXXXXXXX1234",
        "dob": "1990-01-15",
        "status": "1",
        "cached": false
      },
      "vpaChargebackCheck": {
        "vpas": [
          { "vpa": "johndoe@upi", "isBlocklisted": false },
          { "vpa": "9876543210@paytm", "isBlocklisted": false }
        ]
      }
    },
    "completedAt": "2025-01-11T10:30:00.000Z"
  }
}
```

Use `result.vpaChargebackCheck.vpas[].isBlocklisted` to determine blocklist status.

## Webhook Headers

Each webhook request includes these headers:

| Header                | Description                                              |
| --------------------- | -------------------------------------------------------- |
| `Content-Type`        | Always `application/json`                                |
| `X-Webhook-Timestamp` | Unix timestamp (milliseconds) when signature was created |
| `X-Webhook-Signature` | HMAC-SHA256 signature for verification                   |

## Signature Verification

<Warning>
  Always verify webhook signatures to ensure requests are from TxnCheck and haven't been tampered with.
</Warning>

### Verification Algorithm

```
payload_to_sign = `${timestamp}.${JSON.stringify(body)}`
expected_signature = HMAC_SHA256(webhook_secret, payload_to_sign)
```

### Implementation Examples

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(payload, signature, timestamp, secret) {
    // Reject old timestamps (> 5 minutes)
    const currentTime = Date.now();
    if (Math.abs(currentTime - parseInt(timestamp)) > 300000) {
      throw new Error('Timestamp too old');
    }

    // Calculate expected signature
    const payloadToSign = `${timestamp}.${JSON.stringify(payload)}`;
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(payloadToSign)
      .digest('hex');

    // Compare signatures using timing-safe comparison
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    )) {
      throw new Error('Invalid signature');
    }

    return true;
  }

  // Express.js example
  app.post('/webhooks/txncheck', express.json(), (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];
    
    try {
      verifyWebhookSignature(req.body, signature, timestamp, WEBHOOK_SECRET);
      
      const { event, data } = req.body;
      console.log(`Received ${event} for request ${data.requestId}`);
      
      switch (event) {
        case 'request.completed':
          // Process successful verification
          handleVerificationComplete(data);
          break;
        case 'request.failed':
          // Handle failed verification
          handleVerificationFailed(data);
          break;
        case 'request.partial':
          // Handle partial completion
          handlePartialVerification(data);
          break;
      }
      
      res.status(200).json({ received: true });
    } catch (error) {
      console.error('Webhook error:', error.message);
      res.status(400).json({ error: error.message });
    }
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import json
  from flask import Flask, request, jsonify

  app = Flask(__name__)
  WEBHOOK_SECRET = 'your_webhook_secret'

  def verify_webhook_signature(payload, signature, timestamp, secret):
      # Reject old timestamps (> 5 minutes)
      current_time = int(time.time() * 1000)
      if abs(current_time - int(timestamp)) > 300000:
          raise ValueError('Timestamp too old')
      
      # Calculate expected signature
      payload_to_sign = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
      expected_signature = hmac.new(
          secret.encode(),
          payload_to_sign.encode(),
          hashlib.sha256
      ).hexdigest()
      
      # Compare signatures
      if not hmac.compare_digest(signature, expected_signature):
          raise ValueError('Invalid signature')
      
      return True

  @app.route('/webhooks/txncheck', methods=['POST'])
  def webhook_handler():
      signature = request.headers.get('X-Webhook-Signature')
      timestamp = request.headers.get('X-Webhook-Timestamp')
      payload = request.json
      
      try:
          verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET)
          
          event = payload['event']
          data = payload['data']
          print(f"Received {event} for request {data['requestId']}")
          
          if event == 'request.completed':
              handle_verification_complete(data)
          elif event == 'request.failed':
              handle_verification_failed(data)
          elif event == 'request.partial':
              handle_partial_verification(data)
          
          return jsonify({'received': True}), 200
      except ValueError as e:
          return jsonify({'error': str(e)}), 400
  ```

  ```php PHP theme={null}
  <?php
  function verifyWebhookSignature($payload, $signature, $timestamp, $secret) {
      // Reject old timestamps (> 5 minutes)
      $currentTime = time() * 1000;
      if (abs($currentTime - (int)$timestamp) > 300000) {
          throw new Exception('Timestamp too old');
      }
      
      // Calculate expected signature
      $payloadToSign = $timestamp . '.' . json_encode($payload);
      $expectedSignature = hash_hmac('sha256', $payloadToSign, $secret);
      
      // Compare signatures
      if (!hash_equals($signature, $expectedSignature)) {
          throw new Exception('Invalid signature');
      }
      
      return true;
  }

  // Handle incoming webhook
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
  $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
  $payload = json_decode(file_get_contents('php://input'), true);

  try {
      verifyWebhookSignature($payload, $signature, $timestamp, WEBHOOK_SECRET);
      
      $event = $payload['event'];
      $data = $payload['data'];
      
      switch ($event) {
          case 'request.completed':
              handleVerificationComplete($data);
              break;
          case 'request.failed':
              handleVerificationFailed($data);
              break;
          case 'request.partial':
              handlePartialVerification($data);
              break;
      }
      
      http_response_code(200);
      echo json_encode(['received' => true]);
  } catch (Exception $e) {
      http_response_code(400);
      echo json_encode(['error' => $e->getMessage()]);
  }
  ```

  ```go Go theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "fmt"
      "math"
      "net/http"
      "strconv"
      "time"
  )

  func verifyWebhookSignature(payload []byte, signature string, timestamp string, secret string) error {
      // Reject old timestamps (> 5 minutes)
      ts, _ := strconv.ParseInt(timestamp, 10, 64)
      currentTime := time.Now().UnixMilli()
      if math.Abs(float64(currentTime-ts)) > 300000 {
          return fmt.Errorf("timestamp too old")
      }

      // Calculate expected signature
      payloadToSign := fmt.Sprintf("%s.%s", timestamp, string(payload))
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write([]byte(payloadToSign))
      expectedSignature := hex.EncodeToString(mac.Sum(nil))

      // Compare signatures
      if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
          return fmt.Errorf("invalid signature")
      }

      return nil
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      signature := r.Header.Get("X-Webhook-Signature")
      timestamp := r.Header.Get("X-Webhook-Timestamp")

      var payload map[string]interface{}
      json.NewDecoder(r.Body).Decode(&payload)
      payloadBytes, _ := json.Marshal(payload)

      if err := verifyWebhookSignature(payloadBytes, signature, timestamp, webhookSecret); err != nil {
          http.Error(w, err.Error(), http.StatusBadRequest)
          return
      }

      event := payload["event"].(string)
      data := payload["data"].(map[string]interface{})
      fmt.Printf("Received %s for request %s\n", event, data["requestId"])

      w.WriteHeader(http.StatusOK)
      json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }
  ```
</CodeGroup>

## Retry Policy

If webhook delivery fails (non-2xx response or timeout), TxnCheck retries with exponential backoff:

| Attempt | Delay     |
| ------- | --------- |
| 1       | Immediate |
| 2       | 1 second  |
| 3       | 2 seconds |
| 4       | 4 seconds |
| 5       | 8 seconds |

After 5 failed attempts, the webhook is marked as failed. You can still retrieve results using the Request Status endpoint.

## Testing Webhooks

### Local Development

For local development, use a tunneling service like [ngrok](https://ngrok.com) to expose your local server:

```bash theme={null}
# Start ngrok
ngrok http 3000

# Use the ngrok URL for your webhook endpoint
# https://abc123.ngrok.io/webhooks/txncheck
```

### Webhook Logs

View webhook delivery logs in the [Merchant Dashboard](https://dashboard.txncheck.in) under **Webhooks → Logs**. You can see:

* Delivery status (success/failed)
* Response codes
* Retry attempts
* Payload details

## Best Practices

<CardGroup cols={2}>
  <Card title="Always Verify Signatures" icon="shield-check">
    Never process webhooks without verifying the signature first
  </Card>

  <Card title="Respond Quickly" icon="bolt">
    Return 200 immediately, process events asynchronously
  </Card>

  <Card title="Handle Duplicates" icon="copy">
    Use request ID for idempotency - you may receive the same event twice
  </Card>

  <Card title="Log Everything" icon="clipboard-list">
    Log webhook payloads for debugging and audit trails
  </Card>
</CardGroup>

<Note>
  Webhook endpoints must be publicly accessible HTTPS URLs. Self-signed certificates are not supported.
</Note>
