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

    • Open API Keys in your dashboard and click Create key.
    • Give the key a name and choose the server.
    • Tick the permissions. For this walkthrough tick both, config:read and vpn_exclusions:write.
    • Choose how long the key stays valid, or keep the preselected 365 days. The choices are listed under "Keys and permissions" below.
    • Confirm with your password. If you use two-factor authentication, enter a code from your authenticator app or a backup code instead.
    • The key is shown only once. Copy it right away and store it safely.
  2. Put the key and your server ID into two variables, so the commands below work as they are:

    export SONAR_KEY=sonar_...
    export SERVER_UID=123456

    The server ID is a six digit number. You find it next to the server name on the API page of your dashboard and on the page that shows your new key.

    On Windows, type curl.exe instead of curl in PowerShell, write each command on one line without the backslashes, and paste your key and server ID in place of $SONAR_KEY and $SERVER_UID.

  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/whoamiany valid key
GET /api/v1/manage/servers/{uid}/configconfig:read
GET /api/v1/manage/servers/{uid}/messagesconfig:read
GET /api/v1/manage/servers/{uid}/lists/vpn-exclusionsconfig:read
POST /api/v1/manage/servers/{uid}/lists/vpn-exclusions/entriesvpn_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
401invalid_keyThe key is missing, mistyped, unknown, expired or revoked. All five get the same answer on purpose, so a stranger learns nothing about your keys.
403role_revokedThe person who created the key is no longer owner or editor of the server.
403plan_requiredThe plan of the server owner does not include the API (it needs Business or Enterprise).
400bad_requestThe body is not valid JSON, or a required parameter is missing.
403scope_requiredThe 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.
409list_fullThe 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.
422invalid_entryThe type or the value of an entry is not valid.
422config_invalidA 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.
404not_foundUnknown path, a server ID that is not the one of the key, or a server that is deleted, deactivated or paused.
413noneThe 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.
429rate_limitedToo many requests or too many failed key checks. Wait as many seconds as the Retry-After header says.
500internal_errorSomething broke on our side. The answer carries no details.
503api_disabledThe 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());
        }
    }
}