> For the complete documentation index, see [llms.txt](https://docs.antivpn.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.antivpn.io/developers/http-webhooks.md).

# HTTP Webhooks

### How It Works

When an event occurs (e.g. a player is blocked), AntVPN sends an HTTP `POST` request to your configured webhook URL with:

* A JSON body containing the event message
* An `X-AntiVPN-Signature` header (if a webhook secret is configured)
* `Content-Type: application/json`
* `User-Agent: AntiVPN-Webhook/1.0`

***

### Webhook Secret & Signature Verification

When you configure a **webhook secret**, every outgoing request includes an `X-AntiVPN-Signature` header. This header contains an HMAC-SHA256 signature of the raw request body, computed using your secret as the key.

#### Header Format

```
X-AntiVPN-Signature: sha256=<hex-encoded HMAC-SHA256>
```

If no secret is configured, the `X-AntiVPN-Signature` header is **not sent**.

#### How the Signature Is Computed

1. The raw JSON request body is used as the message.
2. An HMAC-SHA256 hash is computed using your webhook secret as the key.
3. The resulting hash is hex-encoded and prefixed with `sha256=`.

```
HMAC-SHA256(request_body, webhook_secret) → hex string → "sha256=" + hex string
```

***

### Verifying the Signature on Your Server

To verify that an incoming webhook request is authentic:

1. Read the **raw request body** (do not parse it first).
2. Compute the HMAC-SHA256 of the raw body using your webhook secret.
3. Compare the computed signature with the value in `X-AntiVPN-Signature`.
4. Use a **constant-time comparison** to prevent timing attacks.

#### Node.js Example

```javascript
const crypto = require("crypto");

function verifyWebhookSignature(req, secret) {
  const signature = req.headers["x-antivpn-signature"];
  if (!signature) {
    return false;
  }

  const rawBody = req.body; // Must be the raw string/buffer, not parsed JSON
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Express.js example
const express = require("express");
const app = express();

// IMPORTANT: Use raw body parser so the body is available as a Buffer
app.use("/webhook", express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const secret = process.env.WEBHOOK_SECRET;

  if (!verifyWebhookSignature(req, secret)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  console.log("Verified webhook event:", event);

  res.status(200).send("OK");
});

app.listen(3000);
```

#### Python Example

```python
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)

WEBHOOK_SECRET = "your-webhook-secret"

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    if not signature:
        return False

    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

@app.route("/webhook", methods=["POST"])
def webhook():
    signature = request.headers.get("X-AntiVPN-Signature", "")
    raw_body = request.get_data()

    if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
        abort(401, "Invalid signature")

    event = request.get_json()
    print("Verified webhook event:", event)

    return "OK", 200
```

#### Go Example

```go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
)

func verifySignature(body []byte, signature, secret string) bool {
	if signature == "" {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

	return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	secret := "your-webhook-secret"

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	signature := r.Header.Get("X-AntiVPN-Signature")
	if !verifySignature(body, signature, secret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	fmt.Println("Verified webhook event:", string(body))
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

func main() {
	http.HandleFunc("/webhook", webhookHandler)
	http.ListenAndServe(":3000", nil)
}
```

#### Java Example

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class WebhookVerifier {

    public static boolean verifySignature(byte[] payload, String signature, String secret)
            throws Exception {
        if (signature == null || signature.isEmpty()) {
            return false;
        }

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(payload);

        StringBuilder hex = new StringBuilder();
        for (byte b : hash) {
            hex.append(String.format("%02x", b));
        }

        String expected = "sha256=" + hex.toString();
        return expected.equals(signature);
    }
}
```

***

### Request Headers

Every HTTP webhook request includes the following headers:

| Header                | Description                                                                 |
| --------------------- | --------------------------------------------------------------------------- |
| `Content-Type`        | Always `application/json`.                                                  |
| `User-Agent`          | Always `AntiVPN-Webhook/1.0`.                                               |
| `X-AntiVPN-Signature` | HMAC-SHA256 signature of the body (only present if a secret is configured). |

***

### Webhook Events

HTTP webhooks can subscribe to the following events:

#### `blocked`

Fired when a player connection is blocked by AntVPN.

**Available placeholders:**

| Placeholder      | Description                                  |
| ---------------- | -------------------------------------------- |
| `{username}`     | The player's username.                       |
| `{userUniqueId}` | The player's unique identifier.              |
| `{ip}`           | The player's IP address.                     |
| `{reason}`       | The reason the connection was blocked.       |
| `{country}`      | The country ISO code of the IP.              |
| `{asn}`          | The ASN number associated with the IP.       |
| `{blocked}`      | Whether the IP was blocked (`true`/`false`). |
| `{riskScore}`    | The risk score assigned to the IP.           |
| `{riskLevel}`    | The risk level category.                     |
| `{provider}`     | The network provider name.                   |

#### `shielded`

Fired when a new attack is detected.

**Available placeholders:**

| Placeholder         | Description                                        |
| ------------------- | -------------------------------------------------- |
| `{attack_rps}`      | Current requests per second.                       |
| `{attack_rpm}`      | Average requests per minute (1-minute window).     |
| `{attack_rpm_5min}` | Average requests per minute (5-minute window).     |
| `{attack_started}`  | Unix timestamp when the attack started.            |
| `{attack_duration}` | Duration of the attack in seconds (initially `0`). |

#### `unshielded`

Fired when an attack has ended.

**Available placeholders:**

| Placeholder                   | Description                                    |
| ----------------------------- | ---------------------------------------------- |
| `{attack_duration}`           | Total attack duration in seconds.              |
| `{attack_started}`            | Unix timestamp when the attack started.        |
| `{attack_ended}`              | Unix timestamp when the attack ended.          |
| `{attack_duration_formatted}` | Human-readable attack duration (e.g. `1m30s`). |

***

### Timeouts & Retries

* Requests have a **10-second timeout**.
* Failed requests are retried up to **3 times**.
* If your server responds with `429 Too Many Requests`, AntVPN will respect the `Retry-After` header (or wait 2 seconds by default) before retrying.

***

### Best Practices

* **Always verify the signature** before processing webhook data.
* **Use constant-time comparison** (e.g. `crypto.timingSafeEqual`, `hmac.compare_digest`, `hmac.Equal`) to prevent timing attacks.
* **Read the raw body** before parsing JSON — signature verification must be done on the exact bytes sent.
* **Respond quickly** with a `2xx` status code. Perform any heavy processing asynchronously after responding.
* **Store your webhook secret securely** (e.g. environment variables, secret managers). Do not hard-code it in your source.
* **Return `401`** if signature verification fails, so issues are easy to diagnose in logs.
