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

# Webhooks

> Learn how to set up and handle webhook notifications in Novac Payment.

## Overview

Webhooks are a communication mechanism that enables event-driven notifications between systems. When an event occurs in Novac system, such as a payment being completed or a payout being initiated. Novac automatically sends a real-time notification to the merchant’s system. This means your system doesn’t have to constantly poll Novac for updates. Instead, you simply provide Novac with a publicly accessible webhook URL, and Novac will send a `POST` request to that endpoint whenever a relevant event occurs.

<Note>
  Webhook URLs must be publicly accessible at all times in order to receive notifications.
</Note>

This whole process makes it easier to keep your system in sync with payment statuses when you integrate with Novac.

## Process Flow

This sequence diagram illustrates the process flow from initiating a checkout payment to its completion, involving the customer, the merchant system, and Novac.

```mermaid theme={null}
    sequenceDiagram
    participant C as Customer
    participant N as Novac Payment System
    participant M as Merchant System

    C->>N: Initiates payment (card/bank/mobile)
    N->>N: Process & authorize payment
    N-->>C: Payment status (success/failure)

    Note over N,M: Webhook event triggered

    N->>M: HTTP POST request (webhook notification)
    M->>N: 200 OK (acknowledge receipt)

    Note over M: Merchant verifies transaction<br/>via Verify Transaction API
```

With Novac payment, you don’t need to manually subscribe to webhook events.\
Once you configure your webhook URLs in the dashboard, Novac automatically sends notifications whenever relevant events occur in your account.

***

## Setting Up Your Webhook

<Steps>
  <Step title="Step 1 - Log in to your Novac Dashboard">
    Sign in to your [**Novac Dashboard**](https://app.novacpayment.com) using your account credentials.
  </Step>

  <Step title="Step 2 - Navigate to Webhooks">
    From the sidebar menu, go to Settings, Click on API settings tab.\
    This is where you can configure both test and live webhook URLs.
  </Step>

  <Step title="Step 3 - Add your Webhook URLs and Click on Save">
    Enter the publicly accessible URLs where you want to receive webhook notifications.
  </Step>
</Steps>

<Note>
  * Add a test webhook URL for test transactions.
  * Add a live webhook URL for real payments in production.
  * Ensure the URLs are always available and accept `POST` requests.
</Note>

Novac will send an HTTP `POST` request to these URLs after a transaction is completed.

***

## Notification Types

| Notify Type  | Description                                        | Common Scenario                                  |
| ------------ | -------------------------------------------------- | ------------------------------------------------ |
| `successful` | Payment or transaction was processed successfully. | A customer’s card or bank transfer succeeded.    |
| `failed`     | Transaction attempt failed or was declined.        | Insufficient funds or incorrect payment details. |
| `reversed`   | A previously successful transaction was reversed.  | Refunds or chargeback scenarios.                 |
| `abandoned`  | Transaction was started but not completed.         | Customer exited before finalizing payment.       |

***

## Handling Webhook payload

Each webhook request payload sent from Novac follows the structure below:

<CodeGroup>
  ```json expandable Collection Webhook Sample theme={null}
  {
    "data": {
      "id": 0,
      "card": {
        "type": "",
        "token": "",
        "issuer": "",
        "country": "",
        "last4Digits": "",
        "first6Digits": ""
      },
      "amount": 300,
      "domain": "live",
      "status": "failed | reversed | successful | abandoned",
      "channel": "",
      "currency": "NGN",
      "customer": {
        "id": 0,
        "name": "",
        "email": "",
        "customerCode": ""
      },
      "requestIp": "",
      "redirectUrl": "",
      "chargedAmount": 0,
      "transactionFee": 0,
      "transferDetail": {
        "bankCode": "",
        "bankName": "",
        "sessionId": "",
        "accountNumber": "",
        "originatorName": "",
        "creditAccountName": "",
        "originatorAccountNumber": ""
      },
      "checkoutMetadata": "{}",
      "authorizationCode": "",
      "paymentDescriptor": "NOVAC",
      "gatewayResponseCode": "",
      "transactionReference": ""
    },
    "notify": "transaction | wallet_funding | banktransfer",
    "notifyType": "failed | reversed | successful | abandoned"
  }
  ```

  ```json Payout Webhook Sample theme={null}
  {
    "data": {
      "id": 1,
      "fee": "50.00",
      "amount": "500.00",
      "domain": "test | live",
      "status": "successful | failed | reversed",
      "currency": "NGN | GHS",
      "bank_code": "123456",
      "bank_name": "Test bank name",
      "narration": "Test Narration",
      "reference": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
      "sessionid": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
      "created_at": "2026-06-19T09:11:18",
      "updated_at": "2026-06-19T08:11:39",
      "countryCode": "NG | GH",
      "account_name": "Test Account Name",
      "account_number": "1234567890",
      "stamp_duty_fee": "50.00"
    },
    "notify": "payout",
    "notifyType": "successful | failed | reversed"
  }
  ```
</CodeGroup>

The webhook structure provides complete details about the transaction event, including customer, card, and transfer details.

<Info>
  For best practices, after receiving a webhook, we recommend that you validate the transaction status by using the [verify transaction endpoint](/docs/accept-payment/manage-payment/verify-transaction).
</Info>

***

## Verifying Webhook Source

Webhooks are publicly available URLs; this means that anyone can fake a webhook sample and send a fake request to your system. It's important to verify that all webhooks received are from Novac to avoid man-in-the-middle attacks.

To verify the webhook your system received.  We advise merchants to allow requests from a dedicated network address. We provide our IP address, and you should verify that every webhook request comes from it. This method is called IP whitelisting.

Novac public IP address:
**18.233.137.110**

<Info>
  It's important to know that we can add to this IP at any point in time; however, we will inform our merchant before doing so.
</Info>

### What is IP whitelisting?

IP whitelisting is a mechanism used on the server-side as a filter; it helps ensure only a pre-approved set of IP addresses or [IP ranges](https://help.clickguard.com/hc/en-us/articles/11875170187933-What-Is-an-IP-Address-Range) have the right to hit a server. If a request comes from an unknown IP address, your server will reject it.

<Note>
  You must use IP whitelisting as a layer of protection for incoming webhooks.
  If your system is behind a [proxy](https://www.fortinet.com/resources/cyberglossary/proxy-server) or [load balancers](https://www.cloudflare.com/learning/performance/what-is-load-balancing/), you must obtain the real client IP.
</Note>

### Validate incoming webhook IP address.

To validate incoming webhooks, we have provided sample codes to give you a better context on how to go about this depending on your programming language.

<CodeGroup>
  ```javascript NodeJs theme={null}
  // simple-ip-whitelist.js
  const express = require('express');
  const app = express();

  const ALLOWED_IPS = ['18.233.137.110']; 

  app.set('trust proxy', true); // check for proxy or load balancer

  function ipWhitelist(req, res, next) {
    const clientIp = (req.ip || '').replace(/^::ffff:/, ''); // normalize IPv4-mapped IPv6
    if (ALLOWED_IPS.includes(clientIp)) return next();
    console.warn(`Blocked webhook from IP: ${clientIp}`);
    return res.status(403).send('Forbidden');
  }

  app.post('/webhook', ipWhitelist, express.json(), (req, res) => {
    // handle verified webhook here
    res.status(200).send({received: true});
  });

  app.listen(3000, () => console.log('Webhook listener on :3000'));

  ```

  ```php PHP theme={null}
  <?php

  header('Content-Type: application/json');

  $ALLOWED_IPS = ['18.233.137.110']; 

  // Get client IP (handles proxies if needed)
  function getClientIp() {
      if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
          $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
          return trim($ips[0]);
      }
      return $_SERVER['REMOTE_ADDR'] ?? '';
  }

  $clientIp = getClientIp();

  if (!in_array($clientIp, $ALLOWED_IPS)) {
      error_log("Blocked webhook from IP: " . $clientIp);
      http_response_code(403);
      echo json_encode(['error' => 'Forbidden']);
      exit;
  }

  // Handle verified webhook
  http_response_code(200);
  echo json_encode(['received' => true]);

  ```

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

  import (
  	"encoding/json"
  	"log"
  	"net"
  	"net/http"
  	"strings"
  )

  var allowedIPs = []string{"18.233.137.110"} 

  func getClientIP(r *http.Request) string {
  	// Try to read X-Forwarded-For if behind proxy
  	xff := r.Header.Get("X-Forwarded-For")
  	if xff != "" {
  		parts := strings.Split(xff, ",")
  		return strings.TrimSpace(parts[0])
  	}
  	ip, _, _ := net.SplitHostPort(r.RemoteAddr)
  	return ip
  }

  func ipWhitelistMiddleware(next http.HandlerFunc) http.HandlerFunc {
  	return func(w http.ResponseWriter, r *http.Request) {
  		clientIP := getClientIP(r)
  		allowed := false
  		for _, ip := range allowedIPs {
  			if clientIP == ip {
  				allowed = true
  				break
  			}
  		}

  		if !allowed {
  			log.Printf("Blocked webhook from IP: %s\n", clientIP)
  			w.WriteHeader(http.StatusForbidden)
  			json.NewEncoder(w).Encode(map[string]string{"error": "Forbidden"})
  			return
  		}
  		next(w, r)
  	}
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  	w.Header().Set("Content-Type", "application/json")
  	json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }

  func main() {
  	http.HandleFunc("/webhook", ipWhitelistMiddleware(webhookHandler))
  	log.Println("Listening on :3000")
  	log.Fatal(http.ListenAndServe(":3000", nil))
  }

  ```

  ```py python (Flask) theme={null}

  from flask import Flask, request, jsonify

  app = Flask(__name__)

  ALLOWED_IPS = ['18.233.137.110'] 

  def get_client_ip():
      # Use X-Forwarded-For if behind proxy
      if 'X-Forwarded-For' in request.headers:
          return request.headers['X-Forwarded-For'].split(',')[0].strip()
      return request.remote_addr

  @app.route('/webhook', methods=['POST'])
  def webhook():
      client_ip = get_client_ip()
      if client_ip not in ALLOWED_IPS:
          app.logger.warning(f"Blocked webhook from IP: {client_ip}")
          return jsonify({'error': 'Forbidden'}), 403
      return jsonify({'received': True}), 200

  if __name__ == '__main__':
      app.run(port=3000)

  ```
</CodeGroup>

<Warning>
  It is crucial to thoroughly validate and properly test this with a test webhook before going live.
</Warning>

In a case where your system is behind a proxy such as nginx. You can do a double validation, e.g., first validate in your config file that the webhook is coming from the Novac network address. If successful, you can then route this webhook to the endpoint defined in your system, which also runs another check to validate the request.

***

## Retries

To ensure reliable delivery, Novac automatically retries webhook notifications when your server fails to respond with a successful 200 OK status code.

<Note>
  Your webhook endpoint must always respond with HTTP 200 to acknowledge receipt of the event. Any other status code (e.g., 4xx or 5xx) will trigger a retry attempt.
</Note>

**Retry Policy**

| Parameter      | Description                                                                         |
| -------------- | ----------------------------------------------------------------------------------- |
| Retry Count    | 3 attempts                                                                          |
| Retry Interval | Every 5 seconds between retries                                                     |
| Condition      | Triggered when Novac doesn’t receive a `200 OK` response from your webhook endpoint |
