# Introduction

Discover AntiVPN.io – Your gateway to seamless installation and advanced anti-VPN solutions. Dive into our guide, and developers can jumpstart with our powerful API.

<figure><img src="/files/L74Yi10GGZqpYVNQkZh2" alt=""><figcaption><p>Server Anti VPN Banner</p></figcaption></figure>

Introducing AntiVPN.io, your ultimate solution for robust VPN detection and player verification. In a world where online security and fair gaming are paramount, we've crafted a comprehensive toolset to empower you. Our state-of-the-art platform is designed to safeguard your online community and ensure a level playing field for all.

**Key Features**

* **Generous Free Tier:** Start with 15k monthly requests, and during BETA, there are no limitations.
* **Analytics**: See where the users come from and more. *(BETA)*
* **High-Performance Cloud Checker:** Harness the power of our cloud-based checker, meticulously optimized for peak performance.
* **Whitelist Control:** Take control of your player base by easily whitelisting individuals you trust. Simply search and authorize them from the intuitive control panel.
* **Sub Accounts (BETA):** Gain enhanced flexibility and control as we work diligently to implement this feature.
* **Cloud Configuration**: Change your AntiVPN config through the panel and get instantly updated.
* **Rest-API**: Implement the AntiVPN in your systems with our brand new Rest-API.

### Upcoming Features

* [x] **Shield Mode:** Safeguard your server during attack times with our advanced Shield Mode.
* [x] **Player Lookup:** Identify users with multiple accounts in your server, ensuring fair play and security.
* [ ] **Custom Rules (Coming Soon):** Tailor your protection with custom rules. Block or unblock specific ASNs, IPs, or countries to match your unique needs.

AntiVPN.io is your trusted partner in the fight against VPN abuse and ensures fair, secure, and enjoyable online experiences for all. Join us today and experience the future of player verification.


# Installation

Follow this step-by-step guide to install the AntiVPN.io plugin for Velocity and BungeeCord.

**Installation Steps**

1. **Download the Plugin:** Begin by downloading the plugin from [here](https://www.spigotmc.org/resources/111768/). Place the downloaded file into your server's plugins folder.
2. **Restart Your Server:** Restart your server to generate the plugin folder. You'll find it named either "serverantivpn" or "ServerAntiVPN."

You need to get the secret token from [dash.antivpn.io](https://antivpn.io). Here is a short video on how to get it.

{% embed url="<https://s.antivpn.io/tutorials/msedge_oWzkm6byZY.mp4>" %}
How to get the AntiVPN secret
{% endembed %}

Then, you can continue and use the following command at the console of your Proxy/Spigot server.

<pre class="language-log"><code class="lang-log"><a data-footnote-ref href="#user-content-fn-1">antivpn</a> <a data-footnote-ref href="#user-content-fn-2">&#x3C;your copied key here></a>
</code></pre>

Then, you're ready to use the AntiVPN in your server :)

[^1]: This is a command, and only works in the console of the Proxy/Spigot server.

[^2]: Here, you paste the code copied from the video below.


# Change Kick Message

Page relocated to the page below.

{% content-ref url="/spaces/1KZJDfxBVQf8xrPDGvAY/pages/ixyzoPPdXJuHpOGPo5Ut" %}
[Creating Configuration](/settings/creating-configuration)
{% endcontent-ref %}


# Random Disconnects

Troubleshooting Random Disconnects from AntiVPN.io

If you are experiencing random disconnects, this is expected behavior due to our use of Cloudflare. These disconnects occur between the AntiVPN plugin and the AntiVPN cloud service, not between regular users and the server. However, there is a way to disable the warnings in the console if you wish.

{% hint style="warning" %}
By disabling this feature, you will cease receiving logs pertaining to AntiVPN.io in your console.
{% endhint %}

1. Go to your plugins folder
2. Then go to ServerAntiVPN plugin folder.
3. Open the config.json file

<pre class="language-json"><code class="lang-json">{
  "debug": false,
  "secret": "your-code-goes-here",
  "detectMessage": "§cVPN Detected!\n§cPlease disable your VPN and rejoin.\n§cIf you believe this is a mistake, please contact an administrator.",
  "cacheTimeout": 120,
  <a data-footnote-ref href="#user-content-fn-1">"level": "INFO"</a>
}
</code></pre>

Please replace the level line with the following configuration:

```json
{
  "level": "OFF"
}
```

[^1]: Edit this line and put OFF


# Creating Configuration

Configure the server and assign it to the appropriate environment.

**Change Steps**

1. First, you go  to the [dash.antivpn.io](https://dash.antivpn.io)
2. If you don't have any server, go to the installation process first. [Click here to see how to install](/getting-started/installation).

Follow this video to get it done.

{% embed url="<https://s.antivpn.io/tutorials/msedge_jX0lMmWxAn.mp4>" %}
Changing the Server Message
{% endembed %}

And that's it. If you see that is not updating or not getting it work, please, open a ticket at [discord.antivpn.io](https://discord.antivpn.io)


# Shield Mode

Automatically safeguard your server from potential attacks or malicious activities.

How to enable Shield Mode.

1. First, you go  to the [dash.antivpn.io](https://dash.antivpn.io)
2. Subsequently, navigate to your server settings. If you do not have one, kindly [click here](/settings/creating-configuration) to learn how to create a configuration.
3. Upon navigating to the settings, enable the Shield mode.

For your reference, please find attached a video demonstrating the step-by-step process of completing this task.

{% embed url="<https://s.antivpn.io/tutorials/shield-mode.mp4>" %}
Enabling Shield mode tutorial
{% endembed %}


# Webhooks


# Workaround

Workaround for some common AntiVPN issues right now.

<figure><img src="/files/jC087Hc7z8L3eAucYigi" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/w9eBc0mpoPz7admyJ6ZC" alt=""><figcaption></figcaption></figure>

If you receive any of these messages, it probably means that your server can't connect to our servers. To see what the issue is here, first we need to do some testing.

Execute this command on your console (if using Pterodactyl, you likely need to contact your hosting provider)

<pre class="language-bash"><code class="lang-bash">curl --request GET \
  --url 'https://connection.antivpn.io/api/check?ip=1.1.1.1' \
  --header 'Authorization: Bearer <a data-footnote-ref href="#user-content-fn-1">SERVER_TOKEN</a>'
</code></pre>

Expected result:

```json
{"ASN":13335,"COUNTRY":"US","VPN":true}
```

If the result is not the expected one, please open a ticket with that information on our Discord server.

[^1]: Your server token same as the config one.


# WebSocket Integration Guide

This document explains how to build your own WebSocket client that connects to the AntVPN real-time protection system. The WebSocket interface is designed for persistent, low-latency communication.

This is ideal for game servers (Minecraft, etc.) that need to verify players in real time as they join.

If you want an already implemented websocket plugin for minecraft, you can use <https://github.com/AntVPN/java-plugin/releases>&#x20;

***

### Base URL

```
wss://api.antivpn.io/connect
```

### Table of Contents

1. Authentication
2. Connection Lifecycle
3. Message Protocol
4. Request Types
   * VERIFY
   * USER\_DATA
   * PING
5. Response Types
   * SETTINGS
   * VERIFY
   * PONG
6. Keep-Alive & Heartbeat
7. Reconnection Strategy
8. Full Examples

***

### Authentication

The WebSocket connection requires a **JWT token** passed in the `Authorization` header during the HTTP upgrade handshake.

```
Authorization: Bearer <your_jwt_token>
```

The JWT token is issued by the AntVPN platform and contains the following claims:

| Claim   | Type   | Description                                |
| ------- | ------ | ------------------------------------------ |
| `uid`   | string | The unique identifier of the token/server. |
| `owner` | string | The account ID of the token owner.         |

If the token is missing, malformed, or expired, the server will reject the connection with an HTTP `401 Unauthorized` or `400 Bad Request` error before the WebSocket upgrade completes.

#### Connection Example (Handshake)

```
GET /connect HTTP/1.1
Host: api.antivpn.io
Upgrade: websocket
Connection: Upgrade
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
```

***

### Connection Lifecycle

Once the WebSocket connection is established, the following sequence occurs:

```
Client                                Server
  |                                      |
  |  ---- WebSocket Upgrade (JWT) ---->  |
  |                                      |  1. Validate JWT
  |                                      |  2. Fetch token data & settings
  |                                      |  3. Upgrade connection
  |  <---- SETTINGS response ----------  |  4. Send current configuration
  |                                      |
  |  ---- VERIFY request ------------->  |  5. Client sends IP checks
  |  <---- VERIFY response -----------  |  6. Server responds with result
  |                                      |
  |  ---- USER_DATA request ---------->  |  7. Client reports player events
  |                                      |
  |  <---- WebSocket PING -----------  |  8. Server sends periodic pings
  |  ---- WebSocket PONG ------------>  |  9. Client must respond with pong
  |                                      |
  |  ---- PING request -------------->  |  10. Client can also send app-level pings
  |  <---- PONG response -------------  |  11. Server responds with pong
  |                                      |
```

#### Step-by-Step

1. **Handshake** — Client opens a WebSocket connection with the JWT in the `Authorization` header.
2. **Settings delivery** — Immediately after the connection is established, the server sends a `SETTINGS` message containing the current protection configuration.
3. **Operational phase** — The client sends `VERIFY` requests for each player that needs to be checked, and `USER_DATA` requests to report player join/leave events.
4. **Heartbeat** — The server sends WebSocket-level `PING` frames every **54 seconds**. The client **must** reply with a `PONG` frame. If no pong is received within **60 seconds**, the server closes the connection.

***

### Message Protocol

All messages are sent as **JSON text frames** over the WebSocket connection. Every message (request or response) includes a `type` field that identifies what kind of message it is.

#### Request Structure

```json
{
  "type": "REQUEST_TYPE",
  ...additional fields
}
```

#### Response Structure

```json
{
  "type": "RESPONSE_TYPE",
  ...additional fields
}
```

***

### Request Types

#### 1. VERIFY Request

Send a `VERIFY` request to check whether a player's IP address should be allowed or blocked. This is the **primary** message type you will use.

```json
{
  "type": "VERIFY",
  "transactionalId": "unique-request-id-123",
  "username": "PlayerName",
  "userId": "player-uuid-here",
  "address": "1.2.3.4",
  "server": "lobby-1"
}
```

| Field             | Type   | Required | Description                                                                                        |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------- |
| `type`            | string | Yes      | Must be `"VERIFY"`.                                                                                |
| `transactionalId` | string | Yes      | A unique identifier for this request. Used to correlate the response back to the original request. |
| `username`        | string | Yes      | The player's in-game username.                                                                     |
| `userId`          | string | Yes      | The player's unique identifier (e.g., Minecraft UUID).                                             |
| `address`         | string | Yes      | The player's IP address (IPv4 or IPv6).                                                            |
| `server`          | string | No       | An optional identifier for the server/proxy the player is connecting to.                           |

**All required fields must be non-empty strings.** If any required field is missing or empty, the request will be silently dropped.

**How Detection Works**

The server evaluates the IP against the following criteria:

1. **Blacklist check** — Is the IP present in the known abuser database?
2. **ASN type check** — Is the ASN type non-residential? (Only `isp`, `business`, `education`, `government`, and `unknown` are considered safe.)
3. **Risk level check** — Is the risk level above `elevated`? (Only `very low`, `low`, and `elevated` are considered safe.)
4. **Username whitelist** — If the username is explicitly whitelisted for the account, the IP check is bypassed entirely.
5. **Custom filters** — Account-specific filters can override the result to force-allow or force-block based on IP, ASN, or country.
6. **Enabled state** — If the protection is disabled in settings, all IPs are allowed regardless.

***

#### 2. USER\_DATA Request

Send a `USER_DATA` request to report player lifecycle events (joins, leaves, server switches). This data is used for analytics, session tracking, and shield-mode intelligence.

```json
{
  "type": "USER_DATA",
  "sessionId": "session-id-from-verify",
  "username": "PlayerName",
  "userId": "player-uuid-here",
  "address": "1.2.3.4",
  "hostname": "play.myserver.com",
  "server": "lobby-1",
  "event": "PLAYER_JOIN",
  "version": "1.20.4",
  "premium": true
}
```

| Field       | Type    | Required | Description                                                                                          |
| ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `type`      | string  | Yes      | Must be `"USER_DATA"`.                                                                               |
| `sessionId` | string  | Yes      | The session ID received from a previous `VERIFY` response, linking this event to the original check. |
| `username`  | string  | Yes      | The player's in-game username.                                                                       |
| `userId`    | string  | Yes      | The player's unique identifier.                                                                      |
| `address`   | string  | Yes      | The player's IP address.                                                                             |
| `hostname`  | string  | Yes      | The hostname the player connected through.                                                           |
| `server`    | string  | No       | The server or sub-server identifier.                                                                 |
| `event`     | string  | No       | The event type. Common values: `"PLAYER_JOIN"`, `"PLAYER_SWITCH"`, `"PLAYER_LEAVE"`.                 |
| `version`   | string  | No       | The client version the player is using (e.g., `"1.20.4"`).                                           |
| `premium`   | boolean | No       | Whether the player has a premium/paid account.                                                       |

**Important:** The `event` field determines how the server tracks online player counts:

* `PLAYER_JOIN` and `PLAYER_SWITCH` increment the online player count.
* Any other event (including `PLAYER_LEAVE`) decrements it.

***

#### 3. PING Request

Send an application-level `PING` to verify the connection is alive. This is **separate** from the WebSocket protocol-level ping/pong frames.

```json
{
  "type": "PING",
  "nonce": "random-nonce-value"
}
```

| Field   | Type   | Required | Description                                                     |
| ------- | ------ | -------- | --------------------------------------------------------------- |
| `type`  | string | Yes      | Must be `"PING"`.                                               |
| `nonce` | string | Yes      | A unique value that will be echoed back in the `PONG` response. |

***

### Response Types

#### 1. SETTINGS Response

Sent by the server **immediately after** the connection is established. Contains the current protection configuration.

```json
{
  "type": "SETTINGS",
  "settings": {
    "enabled": 1,
    "kickMessage": "You have been blocked by AntVPN.",
    "shieldMode": "Under attack? Shield mode will activate automatically.",
    "updatedAt": "2025-01-15T10:30:00Z"
  }
}
```

| Field                  | Type    | Description                                                                |
| ---------------------- | ------- | -------------------------------------------------------------------------- |
| `type`                 | string  | Always `"SETTINGS"`.                                                       |
| `settings.enabled`     | integer | `1` if protection is active, `0` if disabled. When disabled, all IPs pass. |
| `settings.kickMessage` | string  | The message displayed to blocked players.                                  |
| `settings.shieldMode`  | string  | The shield mode message/configuration.                                     |
| `settings.updatedAt`   | string  | Timestamp of the last settings update.                                     |

**Your client should store these settings** and apply them locally. For example, use `kickMessage` as the disconnect reason when blocking a player, and respect the `enabled` flag.

***

#### 2. VERIFY Response

Returned in response to a `VERIFY` request. Tells you whether the player should be allowed or blocked.

```json
{
  "type": "VERIFY",
  "transactionalId": "unique-request-id-123",
  "sessionId": "generated-session-id",
  "userId": "player-uuid-here",
  "username": "PlayerName",
  "serverId": "lobby-1",
  "address": "1.2.3.4",
  "valid": true,
  "is_attack": false
}
```

| Field             | Type    | Description                                                                                                    |
| ----------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `type`            | string  | Always `"VERIFY"`.                                                                                             |
| `transactionalId` | string  | Echoes the `transactionalId` from your request, so you can match responses to requests.                        |
| `sessionId`       | string  | A unique session ID generated by the server. Use this in subsequent `USER_DATA` messages for this player.      |
| `userId`          | string  | The player's unique identifier (echoed back).                                                                  |
| `username`        | string  | The player's username (echoed back).                                                                           |
| `serverId`        | string  | The server identifier (echoed back).                                                                           |
| `address`         | string  | The player's IP address (echoed back).                                                                         |
| `valid`           | boolean | **`true`** = allow the player. **`false`** = block the player.                                                 |
| `is_attack`       | boolean | `true` if the system has detected a bot attack is in progress and has engaged shield mode for this connection. |

**How to Act on the Response**

```
if valid == true:
    → Allow the player to join.
    → Send a USER_DATA event with event="PLAYER_JOIN" using the returned sessionId.

if valid == false:
    → Block the player.
    → Kick them with the kickMessage from the SETTINGS response.
```

***

#### 3. PONG Response

Returned in response to an application-level `PING` request.

```json
{
  "type": "PONG",
  "nonce": "random-nonce-value"
}
```

| Field   | Type   | Description                                          |
| ------- | ------ | ---------------------------------------------------- |
| `type`  | string | Always `"PONG"`.                                     |
| `nonce` | string | The same nonce value you sent in the `PING` request. |

***

### Keep-Alive & Heartbeat

The WebSocket connection uses a dual-layer keep-alive mechanism:

#### WebSocket Protocol-Level Pings (Mandatory)

* The **server** sends WebSocket `PING` frames every **\~54 seconds**.
* Your client **must** respond with a WebSocket `PONG` frame automatically. Most WebSocket libraries handle this by default.
* If the server does not receive a `PONG` within **60 seconds**, it will **close the connection**.
* The **write deadline** for each message is **10 seconds**. If a write takes longer, the connection is terminated.

#### Application-Level Pings (Optional)

* Your client can send `PING` messages (JSON) at any time to verify the connection is responsive.
* The server will reply with a `PONG` message containing the same `nonce`.
* This is useful for measuring round-trip latency.

#### Timeout Summary

| Parameter     | Value      | Description                                   |
| ------------- | ---------- | --------------------------------------------- |
| Write timeout | 10 seconds | Maximum time to write a single message.       |
| Pong timeout  | 60 seconds | Maximum time to wait for a pong after a ping. |
| Ping interval | 54 seconds | How often the server sends ping frames.       |

***

### Reconnection Strategy

Your client should implement automatic reconnection with the following best practices:

1. **Detect disconnections** — Listen for WebSocket close events and unexpected errors.
2. **Exponential backoff** — Wait before reconnecting, increasing the delay with each consecutive failure (e.g., 1s → 2s → 4s → 8s → max 30s).
3. **Re-authenticate** — Each new connection requires the full JWT handshake. There is no session resumption.
4. **Re-apply settings** — After reconnecting, wait for the new `SETTINGS` message and update your local configuration.
5. **Re-verify active players** — Any players who joined while disconnected should be re-verified.

#### Example Reconnection Logic (Pseudocode)

```
maxRetries = unlimited
baseDelay = 1 second
maxDelay = 30 seconds
currentDelay = baseDelay

loop:
    try:
        connect(url, jwt)
        currentDelay = baseDelay   // Reset on success
        handleMessages()           // Blocks until disconnect
    catch:
        log("Disconnected, retrying in {currentDelay}...")
        sleep(currentDelay)
        currentDelay = min(currentDelay * 2, maxDelay)
```

***

### Full Examples

#### JavaScript (Browser / Node.js)

```javascript
const WebSocket = require("ws");

const WS_URL = "wss://api.antivpn.io/connect";
const JWT_TOKEN = "your_jwt_token_here";

let ws;
let kickMessage = "Blocked by AntVPN.";
const pendingChecks = new Map();

function connect() {
  ws = new WebSocket(WS_URL, {
    headers: {
      Authorization: `Bearer ${JWT_TOKEN}`,
    },
  });

  ws.on("open", () => {
    console.log("Connected to AntVPN");
  });

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw);

    switch (msg.type) {
      case "SETTINGS":
        console.log("Protection enabled:", msg.settings.enabled === 1);
        kickMessage = msg.settings.kickMessage;
        break;

      case "VERIFY":
        const callback = pendingChecks.get(msg.transactionalId);
        if (callback) {
          callback(msg);
          pendingChecks.delete(msg.transactionalId);
        }
        break;

      case "PONG":
        console.log("Pong received, nonce:", msg.nonce);
        break;
    }
  });

  ws.on("close", () => {
    console.log("Disconnected. Reconnecting in 5s...");
    setTimeout(connect, 5000);
  });

  ws.on("error", (err) => {
    console.error("WebSocket error:", err.message);
  });
}

function verifyPlayer(username, userId, ip, server) {
  return new Promise((resolve) => {
    const txId = `tx_${Date.now()}_${Math.random().toString(36).slice(2)}`;

    pendingChecks.set(txId, (response) => {
      resolve({
        allowed: response.valid,
        sessionId: response.sessionId,
        isAttack: response.is_attack,
      });
    });

    ws.send(JSON.stringify({
      type: "VERIFY",
      transactionalId: txId,
      username,
      userId,
      address: ip,
      server,
    }));
  });
}

function reportPlayerJoin(sessionId, username, userId, ip, hostname, server, version, premium) {
  ws.send(JSON.stringify({
    type: "USER_DATA",
    sessionId,
    username,
    userId,
    address: ip,
    hostname,
    server,
    event: "PLAYER_JOIN",
    version,
    premium,
  }));
}

function reportPlayerLeave(sessionId, username, userId, ip, hostname, server) {
  ws.send(JSON.stringify({
    type: "USER_DATA",
    sessionId,
    username,
    userId,
    address: ip,
    hostname,
    server,
    event: "PLAYER_LEAVE",
  }));
}

// Start
connect();

// Usage example:
// const result = await verifyPlayer("Steve", "uuid-1234", "1.2.3.4", "lobby");
// if (!result.allowed) kickPlayer("Steve", kickMessage);
// else reportPlayerJoin(result.sessionId, "Steve", "uuid-1234", "1.2.3.4", "play.example.com", "lobby", "1.20.4", true);
```

#### Java

```java
import javax.websocket.*;
import java.net.URI;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

@ClientEndpoint
public class AntVPNClient {

    private static final String WS_URL = "wss://api.antivpn.io/connect";
    private static final Gson GSON = new Gson();

    private Session session;
    private String jwtToken;
    private String kickMessage = "Blocked by AntVPN.";
    private boolean protectionEnabled = true;
    private final Map<String, Consumer<JsonObject>> pendingChecks = new ConcurrentHashMap<>();

    public AntVPNClient(String jwtToken) {
        this.jwtToken = jwtToken;
    }

    public void connect() throws Exception {
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        ClientEndpointConfig config = ClientEndpointConfig.Builder.create()
            .configurator(new ClientEndpointConfig.Configurator() {
                @Override
                public void beforeRequest(Map<String, java.util.List<String>> headers) {
                    headers.put("Authorization", java.util.List.of("Bearer " + jwtToken));
                }
            })
            .build();
        container.connectToServer(this, config, new URI(WS_URL));
    }

    @OnMessage
    public void onMessage(String raw) {
        JsonObject msg = GSON.fromJson(raw, JsonObject.class);
        String type = msg.get("type").getAsString();

        switch (type) {
            case "SETTINGS":
                JsonObject settings = msg.getAsJsonObject("settings");
                protectionEnabled = settings.get("enabled").getAsInt() == 1;
                kickMessage = settings.get("kickMessage").getAsString();
                System.out.println("Settings received. Enabled: " + protectionEnabled);
                break;

            case "VERIFY":
                String txId = msg.get("transactionalId").getAsString();
                Consumer<JsonObject> callback = pendingChecks.remove(txId);
                if (callback != null) {
                    callback.accept(msg);
                }
                break;

            case "PONG":
                System.out.println("Pong: " + msg.get("nonce").getAsString());
                break;
        }
    }

    public void verifyPlayer(String username, String userId, String ip, String server,
                             Consumer<JsonObject> callback) {
        String txId = UUID.randomUUID().toString();
        pendingChecks.put(txId, callback);

        JsonObject req = new JsonObject();
        req.addProperty("type", "VERIFY");
        req.addProperty("transactionalId", txId);
        req.addProperty("username", username);
        req.addProperty("userId", userId);
        req.addProperty("address", ip);
        req.addProperty("server", server);

        session.getAsyncRemote().sendText(GSON.toJson(req));
    }

    public void reportEvent(String sessionId, String username, String userId,
                            String ip, String hostname, String server,
                            String event, String version, boolean premium) {
        JsonObject req = new JsonObject();
        req.addProperty("type", "USER_DATA");
        req.addProperty("sessionId", sessionId);
        req.addProperty("username", username);
        req.addProperty("userId", userId);
        req.addProperty("address", ip);
        req.addProperty("hostname", hostname);
        req.addProperty("server", server);
        req.addProperty("event", event);
        req.addProperty("version", version);
        req.addProperty("premium", premium);

        session.getAsyncRemote().sendText(GSON.toJson(req));
    }

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        System.out.println("Connected to AntVPN WebSocket");
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        System.out.println("Disconnected: " + reason.getReasonPhrase());
        // Implement reconnection logic here
    }

    public String getKickMessage() {
        return kickMessage;
    }

    public boolean isProtectionEnabled() {
        return protectionEnabled;
    }
}
```

#### Python

```python
import asyncio
import json
import uuid
import websockets

WS_URL = "wss://api.antivpn.io/connect"
JWT_TOKEN = "your_jwt_token_here"

kick_message = "Blocked by AntVPN."
pending_checks: dict[str, asyncio.Future] = {}


async def connect():
    global kick_message

    headers = {"Authorization": f"Bearer {JWT_TOKEN}"}

    async with websockets.connect(WS_URL, extra_headers=headers) as ws:
        print("Connected to AntVPN")

        async for raw in ws:
            msg = json.loads(raw)
            msg_type = msg.get("type")

            if msg_type == "SETTINGS":
                settings = msg["settings"]
                kick_message = settings["kickMessage"]
                print(f"Protection enabled: {settings['enabled'] == 1}")

            elif msg_type == "VERIFY":
                tx_id = msg["transactionalId"]
                if tx_id in pending_checks:
                    pending_checks[tx_id].set_result(msg)

            elif msg_type == "PONG":
                print(f"Pong received, nonce: {msg['nonce']}")


async def verify_player(ws, username: str, user_id: str, ip: str, server: str) -> dict:
    tx_id = str(uuid.uuid4())
    future = asyncio.get_event_loop().create_future()
    pending_checks[tx_id] = future

    await ws.send(json.dumps({
        "type": "VERIFY",
        "transactionalId": tx_id,
        "username": username,
        "userId": user_id,
        "address": ip,
        "server": server,
    }))

    result = await asyncio.wait_for(future, timeout=10.0)
    del pending_checks[tx_id]
    return result


async def report_event(ws, session_id: str, username: str, user_id: str,
                       ip: str, hostname: str, server: str, event: str,
                       version: str = "", premium: bool = False):
    await ws.send(json.dumps({
        "type": "USER_DATA",
        "sessionId": session_id,
        "username": username,
        "userId": user_id,
        "address": ip,
        "hostname": hostname,
        "server": server,
        "event": event,
        "version": version,
        "premium": premium,
    }))


if __name__ == "__main__":
    asyncio.run(connect())
```

***

### Shield Mode & Attack Detection

AntVPN includes an automatic **Shield Mode** that activates when a bot attack is detected:

* The server tracks the rate of incoming `VERIFY` requests per connection.
* When the request rate exceeds the configured threshold, an attack state is activated.
* During an attack, the system **restricts joins to countries that historically make up the server's player base** (countries representing more than 10% of total joins in the last month).
* The `is_attack` field in `VERIFY` responses indicates whether shield mode allowed the join during an active attack.
* When the attack ends, the system automatically deactivates shield mode.

You do **not** need to implement any shield logic on your side — just respect the `valid` field in `VERIFY` responses.

***

### Error Handling Checklist

| Scenario                           | What happens                              | What you should do                          |
| ---------------------------------- | ----------------------------------------- | ------------------------------------------- |
| Invalid/expired JWT                | Connection rejected with HTTP 400/401     | Re-fetch a valid JWT and retry              |
| Server sends WebSocket close frame | Connection terminates                     | Reconnect with exponential backoff          |
| No pong received within 60s        | Server closes the connection              | Ensure your library handles pong frames     |
| VERIFY response not received       | Possible server issue or message lost     | Implement a timeout (e.g., 10s) per check   |
| `valid` field is `true` but attack | Player allowed, but shield mode is active | Allow the join, the server already filtered |
| Empty or malformed request         | Server silently drops the message         | Validate all fields before sending          |

***

### Summary

| Feature        | Detail                                         |
| -------------- | ---------------------------------------------- |
| Protocol       | WebSocket (RFC 6455) over TLS                  |
| Endpoint       | `wss://api.antivpn.io/connect`                 |
| Auth           | JWT Bearer token in upgrade headers            |
| Message format | JSON text frames                               |
| Request types  | `VERIFY`, `USER_DATA`, `PING`                  |
| Response types | `SETTINGS`, `VERIFY`, `PONG`                   |
| Heartbeat      | Server pings every 54s, 60s pong timeout       |
| Reconnection   | Client responsibility, use exponential backoff |


# Rest-API

AntiVPN Rest-API access.

Before start, you need the **SERVER\_TOKEN**, to generate it you can go to [Installation](/getting-started/installation). It's the same **SERVER\_TOKEN** used for the **Minecraft Plugin**.

#### Using CURL via Bash terminal.

```sh
curl --request GET \
  --url 'https://api.antivpn.io/api/check?ip=1.1.1.1' \
  --header 'Authorization: Bearer SERVER_TOKEN'
```


# HTTP Webhooks

HTTP webhooks allow you to receive real-time event notifications from AntVPN via standard HTTP POST requests to your own server

### 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.


# Check

The Check API is a RESTful HTTP endpoint that allows you to verify whether a given IP address is associated with a VPN, proxy, or any other anon ip provider.

### Base URL

```
https://api.antivpn.io
```

***

### Endpoint

```
GET /v2/check?ip={ip_address}
```

| Parameter | Type   | Required | Description                        |
| --------- | ------ | -------- | ---------------------------------- |
| `ip`      | string | Yes      | The IPv4 or IPv6 address to check. |

***

### Authentication

All requests **must** include a valid API key in the `Authorization` header using the **Bearer** scheme.

```
Authorization: Bearer av_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

#### API Key Format

* Prefix: `av_live_`
* Total length: **72 characters** (`av_live_` + 64 hex characters)
* The hex portion must only contain characters `0-9`, `a-f`, or `A-F`.

If the key does not match this format, the request will be rejected immediately with a `401 Unauthorized` response.

***

### Response Format

All responses are returned as `application/json`.

#### Successful Response — `200 OK`

When the IP address is valid and the lookup succeeds, you will receive:

```json
{
  "success": true,
  "ip": "1.2.3.4",
  "proxy": true,
  "risk_score": 85,
  "processing_time": "12ms",
  "details": {
    "asn": "AS12345",
    "provider": "Example Hosting Inc.",
    "country": "US"
  }
}
```

| Field              | Type    | Description                                                                                               |
| ------------------ | ------- | --------------------------------------------------------------------------------------------------------- |
| `success`          | boolean | Always `true` for successful lookups.                                                                     |
| `ip`               | string  | The IP address that was checked.                                                                          |
| `proxy`            | boolean | `true` if the IP is detected as a VPN, proxy, or non-residential anonymizer. `false` if it appears clean. |
| `risk_score`       | integer | A numeric score (0–100) indicating the abuse confidence level of the IP. Higher values mean higher risk.  |
| `processing_time`  | string  | Server-side latency for the lookup, formatted as milliseconds (e.g. `"5ms"`).                             |
| `details`          | object  | Additional network information about the IP.                                                              |
| `details.asn`      | string  | The Autonomous System Number in `AS{number}` format (e.g. `"AS13335"`). `"Unknown"` if unavailable.       |
| `details.provider` | string  | The name of the network provider or ISP. Falls back to the ASN type if the description is empty.          |
| `details.country`  | string  | ISO 3166-1 alpha-2 country code of the IP (e.g. `"US"`, `"DE"`). `"Unknown"` if unavailable.              |

***

#### Error Responses

All error responses share the same structure:

```json
{
  "success": false,
  "error": "error_code",
  "message": "Human-readable explanation."
}
```

| HTTP Status | `error`          | `message`                        | Cause                                                           |
| ----------- | ---------------- | -------------------------------- | --------------------------------------------------------------- |
| `401`       | `unauthorized`   | `Invalid API key.`               | Missing `Authorization` header, invalid format, or unknown key. |
| `400`       | `invalid_ip`     | `Missing or invalid IP address.` | The `ip` query parameter is missing or not a valid IP address.  |
| `500`       | `internal_error` | `Failed to check IP address.`    | An internal error occurred while looking up the IP.             |

***

### Proxy Detection Logic

An IP is flagged as `proxy: true` when **any** of the following conditions are met:

1. **Blacklisted IP** — The IP is present in the known abuser/blacklist database.
2. **Non-residential ASN type** — The ASN type is **not** one of: `isp`, `business`, `education`, `government`, or `unknown`.
3. **High risk level** — The risk level extracted from the abuse score is **not** one of: `very low`, `low`, or `elevated` (i.e., the IP has a `high` or `very high` risk level).

If none of these conditions apply, the IP is considered clean and `proxy` will be `false`.

#### Risk Score

The `risk_score` is an integer derived from the abuse confidence percentage associated with the IP's ASN data. A score of `0` means no abuse data was found.

***

### Rate Limiting & Analytics

* Every request is logged and counted toward your account's usage analytics (approved vs. blocked counts).
* The `last_used_at` timestamp on your API key is updated (debounced to once per minute).
* If your account exceeds its plan limits, requests may be restricted.

***

### Example Requests

#### cURL

```bash
curl -X GET "https://api.antivpn.io/v2/check?ip=1.2.3.4" \
  -H "Authorization: Bearer av_live_your_api_key_here"
```

#### Python

```python
import requests

headers = {
    "Authorization": "Bearer av_live_your_api_key_here"
}

response = requests.get(
    "https://api.antivpn.io/v2/check",
    params={"ip": "1.2.3.4"},
    headers=headers
)

data = response.json()

if data["success"]:
    if data["proxy"]:
        print(f"Blocked: {data['ip']} (risk: {data['risk_score']}, provider: {data['details']['provider']})")
    else:
        print(f"Allowed: {data['ip']}")
else:
    print(f"Error: {data['message']}")
```

#### JavaScript (Node.js)

```javascript
const response = await fetch("https://api.antivpn.io/v2/check?ip=1.2.3.4", {
  method: "GET",
  headers: {
    "Authorization": "Bearer av_live_your_api_key_here"
  }
});

const data = await response.json();

if (data.success) {
  console.log(`Proxy: ${data.proxy}, Risk: ${data.risk_score}`);
  console.log(`ASN: ${data.details.asn}, Provider: ${data.details.provider}, Country: ${data.details.country}`);
} else {
  console.error(`Error: ${data.message}`);
}
```

#### Java

```java
import java.net.http.*;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.antivpn.io/v2/check?ip=1.2.3.4"))
    .header("Authorization", "Bearer av_live_your_api_key_here")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

***

### Best Practices

* **Cache results** — If you are checking the same IP multiple times in a short window, cache the result on your side to reduce API calls.
* **Validate IPs client-side** — Avoid sending obviously invalid IPs (e.g., empty strings, local addresses) to save quota.
* **Handle errors gracefully** — Always check the `success` field before reading other fields. Network or server errors can happen.
* **Keep your API key secret** — Never expose your `av_live_` key in client-side code, public repositories, or logs.


