> 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/websocket-integration-guide.md).

# WebSocket Integration Guide

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 |
