---
title: API - Sonar
description: Lightweight and easy-to-use anti-bot plugin for your server
url: https://sonar.top/docs/api
language: en
---

# API

The Sonar API lets your own software talk to Sonar. The main use: manage the VPN exclusion list of your server from your own plugin, bot or billing system. For example, a player buys a rank and your shop adds their IP address, so the VPN check no longer blocks them.

You can also read your server settings and the messages your players see. A machine readable description of every request is in [openapi.json](https://sonar.top/api/v1/manage/openapi.json) (OpenAPI 3.1, a standard format that many tools can import).

## Quickstart

1. Create a key. The API is part of the Business and Enterprise plans, and your email address must be confirmed.

2. Put the key and your server ID into two variables, so the commands below work as they are:

3. Test the key. This changes nothing: 
```
curl -H "Authorization: Bearer $SONAR_KEY" \
  https://sonar.top/api/v1/manage/whoami
```

4. Read your server settings: 
```
curl -H "Authorization: Bearer $SONAR_KEY" \
  https://sonar.top/api/v1/manage/servers/$SERVER_UID/config
```

5. Read the VPN exclusion list: 
```
curl -H "Authorization: Bearer $SONAR_KEY" \
  https://sonar.top/api/v1/manage/servers/$SERVER_UID/lists/vpn-exclusions
```

The answer lists every entry and the maximum size of each list:

```
{
  "entries": [
    { "type": "ip", "value": "203.0.113.7" },
    { "type": "cidr", "value": "198.51.100.16/28" },
    { "type": "asn", "value": "64500" },
    { "type": "username", "value": "Steve" }
  ],
  "limits": { "ip": 500, "asn": 200, "username": 500 }
}
```

6. Add an entry to the VPN exclusion list. This needs `vpn_exclusions:write`: 
```
curl -X POST -H "Authorization: Bearer $SONAR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "ip", "value": "203.0.113.7"}' \
  https://sonar.top/api/v1/manage/servers/$SERVER_UID/lists/vpn-exclusions/entries
```

The answer is the entry as it was stored:

```
{ "entry": { "type": "ip", "value": "203.0.113.7" } }
```

7. Remove the entry again. The answer is `204` with an empty body: 
```
curl -X DELETE -H "Authorization: Bearer $SONAR_KEY" \
  "https://sonar.top/api/v1/manage/servers/$SERVER_UID/lists/vpn-exclusions/entries?type=ip&value=203.0.113.7"
```

Answers are JSON. The answers for the settings and the messages carry a short fingerprint of the current state (the `ETag` header). Send it back in the `If-None-Match` header. While nothing changed, you get `304 Not Modified` with an empty body, so you do not have to read the same settings again.

## When a change reaches your server

A change through the API is saved at once. Right after that, Sonar sends the same "settings changed" signal that a save in the dashboard sends. So your server gets an API change the same way and just as fast as a change you make in the dashboard. You do not need to do anything else.

## Endpoints

In the paths, `{uid}` stands for your server ID.

  | Request | Permission |
| --- | --- |
   | `GET /api/v1/manage/whoami` | any valid key |
 | `GET /api/v1/manage/servers/{uid}/config` | `config:read` |
 | `GET /api/v1/manage/servers/{uid}/messages` | `config:read` |
 | `GET /api/v1/manage/servers/{uid}/lists/vpn-exclusions` | `config:read` |
 | `POST /api/v1/manage/servers/{uid}/lists/vpn-exclusions/entries` | `vpn_exclusions:write` |
 | `DELETE /api/v1/manage/servers/{uid}/lists/vpn-exclusions/entries?type=&value=` | `vpn_exclusions:write` |

`POST` takes a JSON body `{"type": "ip", "value": "203.0.113.7"}` and answers `200` with the entry, whether it was added or already there. `DELETE` takes `type` and `value` as query parameters and answers `204`, whether it was removed or already gone. Both can be repeated safely.

The messages are the texts your players see from Sonar: the kick messages (for example when a VPN is blocked or the verification fails), the title during the verification, and the replies to Sonar commands.

## Keys and permissions

Each key carries one or more permissions. Permission and scope mean the same thing: the dashboard says permission, the API says scope, for example in `missing_scopes`.

- Every key is bound to one server. It works for that server only. Any other server ID answers `404`.
- Owners and editors of the server can create keys.
- Keys are valid for 365 days by default. You can pick 30 to 365 days, a date of your own, or no expiration. The server owner gets an email 14 days before a key expires. A server can have 10 active keys.
- `config:read` reads the server settings, the messages and the VPN exclusion list. `vpn_exclusions:write` adds and removes entries of the VPN exclusion list. Writing does not include reading, so choose both if you need both.
- Keys that change all server settings or the messages are planned and not available yet.

### VPN exclusion entries

The Sonar plugin checks joining players for VPNs and proxies. A player who matches an entry of the VPN exclusion list skips that check. It is the same list as the excluded IPs, ASNs and usernames in the proxy detection settings of your dashboard.

An entry is `{"type": "ip", "value": "203.0.113.7"}`. The value is always a string, also for an ASN.

- `ip`: one IPv4 or IPv6 address, for example `203.0.113.7`.
- `cidr`: a block of addresses, written as an address, a slash and a number, for example `198.51.100.16/28`. The number says how many leading bits of the address are fixed. The bigger the number, the smaller the block: `/24` is 256 addresses, `/28` is 16 addresses (198.51.100.16 to 198.51.100.31). Sonar stores the first address of the block. Your keys can add IPv4 blocks from /24 (256 addresses) to /32 and IPv6 blocks from /48 to /128. A larger block, such as /16, answers `403 scope_required`. IP and CIDR entries share a limit of 500.
- `asn`: the number of a provider network, for example of a hosting company, from 1 to 4294967295. An ASN entry lets every address of that network skip the check, so only a wider key may add one. Your keys can read and remove ASN entries, but not add them. At most 200 entries.
- `username`: at most 16 characters, at most 500 entries.

## What whoami tells you

`GET /api/v1/manage/whoami` shows what the key is, without changing anything:

```
{
  "server_uid": "123456",
  "name": "Shop bot",
  "class": "narrow",
  "scopes": ["config:read", "vpn_exclusions:write"],
  "expires_at": "2027-01-27T09:30:00Z"
}
```

- `server_uid`: your server ID, the one server this key works for.
- `name`: the name you gave the key in the dashboard.
- `class`: always `narrow` today, you can ignore it.
- `scopes`: the permissions of the key.
- `expires_at`: when the key stops working, as an ISO 8601 time in UTC, or `null` for a key that never expires.

## Renewing a key

- Renew a key before it expires: open **API Keys** in your dashboard and click **Renew** next to the key. Only the person who created the key sees that button.
- You get a new key with the same name and permissions and a lifetime you choose. It is shown once, like at creation. Put it into your plugin or script.
- The old key keeps working for up to 7 days, so you have time to switch. It never works longer than its own expiry date. The list shows it as "Replaced, valid until" and the date.
- Each key can be renewed once. The new key can be renewed again later. An expired or revoked key cannot be renewed: create a new one instead.
- Renewing needs the same as creating: you are still owner or editor of the server, and the owner's plan includes the API.

## When a key stops working

  | What happened | What the API answers |
| --- | --- |
   | The expiry date has passed. | `401 invalid_key` |
 | Someone revoked the key in the dashboard. | `401 invalid_key` |
 | The person who created the key changed their password, changed their email address (once the new address is confirmed), turned off two-factor authentication, or their account was locked or deleted. The key is revoked. | `401 invalid_key` |
 | The account of the server owner was deleted. All keys of the owner's servers are revoked, including those of editors. | `401 invalid_key` |
 | The server was deleted. Its keys are revoked. | `401 invalid_key` |
 | The server is paused or deactivated. | `404 not_found` |
 | The person who created the key is no longer owner or editor of the server. | `403 role_revoked` |
 | The plan of the server owner no longer includes the API. | `403 plan_required` |

- The server owner gets an email for every revoked key, also when the key is revoked by hand in the dashboard. It names the key and the reason.
- A revoked or expired key never works again. Create a new key.
- A lost role or plan does not revoke the key. The dashboard shows it as inactive. It works again when the role or the plan comes back, until its expiry date.

## Errors

Errors are JSON with `type`, `title` and `status`, sometimes also `detail` (the standard format `application/problem+json`, RFC 9457). Check `type` in your code. It never changes, while the texts may.

  | Status | type | Meaning |
| --- | --- | --- |
   | 401 | `invalid_key` | The key is missing, mistyped, unknown, expired or revoked. All five get the same answer on purpose, so a stranger learns nothing about your keys. |
 | 403 | `role_revoked` | The person who created the key is no longer owner or editor of the server. |
 | 403 | `plan_required` | The plan of the server owner does not include the API (it needs Business or Enterprise). |
 | 400 | `bad_request` | The body is not valid JSON, or a required parameter is missing. |
 | 403 | `scope_required` | The key lacks a permission (see missing\_scopes), or you tried something only a wider key may do: a CIDR block larger than /24 (IPv4) or /48 (IPv6), or any ASN. |
 | 409 | `list_full` | The list is full. limit is the maximum number of entries. legacy is true if the list was already over the limit before your request. Remove entries first. |
 | 422 | `invalid_entry` | The type or the value of an entry is not valid. |
 | 422 | `config_invalid` | A field of your stored server settings is no longer valid, so nothing can be saved. detail names the fields. Fix the settings in the dashboard first. |
 | 404 | `not_found` | Unknown path, a server ID that is not the one of the key, or a server that is deleted, deactivated or paused. |
 | 413 | `none` | The request body is over 64 KB. Our proxy refuses it before it reaches the API, so this answer is not in the JSON error format. |
 | 429 | `rate_limited` | Too many requests or too many failed key checks. Wait as many seconds as the Retry-After header says. |
 | 500 | `internal_error` | Something broke on our side. The answer carries no details. |
 | 503 | `api_disabled` | The API is switched off for maintenance. |

## Rate limits

- Requests are counted per account of the server owner, per minute, over all keys of all its servers. The default is 60 per minute. Your plan can raise it.
- Answers carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the minute ends).
- A `429` carries `Retry-After`. Wait that many seconds.
- Changes to a server are limited to 30 per minute and 500 per day, over all its keys.
- Each IP address can send 300 requests per minute to the API. Repeated requests with a wrong key lock that address out for a while. A valid key is not affected by that lock.

## Security notes

- Send the key only in the `Authorization` header, never in a URL or query string. URLs end up in logs and browser history.
- Never commit a key to a repository and never put it in a plugin that you share. Load it from an environment variable or a secret store.
- If you suspect a leak, revoke the key in the dashboard at once. A revoked key stops working immediately.
- Keys are bound to one server. A leaked key cannot touch your other servers.
- Username exclusions can be spoofed on servers in offline mode: anyone can join with that name and skip the VPN check. Prefer IP or CIDR entries there.

## Java examples

Both examples use `java.net.http.HttpClient` (Java 11 or newer). The key and the server ID come from the environment.

In a plugin, never run these calls on the main server thread: a slow answer would freeze your server. Run them in the background, for example with `Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> ...)` on Bukkit and Paper.

### Read the server settings

```
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SonarConfig {
    public static void main(String[] args) throws Exception {
        String key = System.getenv("SONAR_KEY");
        String uid = System.getenv("SERVER_UID");

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://sonar.top/api/v1/manage/servers/" + uid + "/config"))
            .header("Authorization", "Bearer " + key)
            .header("Accept", "application/json")
            .GET()
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            System.out.println(response.body());
        } else {
            // An error: check the "type" field of the JSON body
            System.err.println(response.statusCode() + " " + response.body());
        }
    }
}
```

### Add an IP address to the VPN exclusion list

```
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SonarExclude {
    public static void main(String[] args) throws Exception {
        String key = System.getenv("SONAR_KEY");
        String uid = System.getenv("SERVER_UID");
        String body = "{\"type\": \"ip\", \"value\": \"203.0.113.7\"}";

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://sonar.top/api/v1/manage/servers/" + uid + "/lists/vpn-exclusions/entries"))
            .header("Authorization", "Bearer " + key)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            System.out.println("Added: " + response.body());
        } else {
            // An error: check the "type" field of the JSON body
            System.err.println(response.statusCode() + " " + response.body());
        }
    }
}
```
